Você usa goto nos seus códigos em C?

- por Sergio Prado

Categorias: Linguagem C Tags: , ,

Esses dias estava trabalhando em um device driver para Linux de um display LCD de 3.5", e me deparei com o código abaixo, responsável pela inicialização do display:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
static int __init s3c24xxfb_probe(struct platform_device *pdev,
                                  enum s3c_drv_type drv_type)
{
    struct s3c2410fb_info *info;
    struct s3c2410fb_display *display;
    struct fb_info *fbinfo;
    struct s3c2410fb_mach_info *mach_info;
    struct resource *res;
    int ret;
    int irq;
    int i;
    int size;
    u32 lcdcon1;
 
    mach_info = pdev->dev.platform_data;
    if (mach_info == NULL) {
        dev_err(&pdev->dev,
                "no platform data for lcd, cannot attach\n");
        return -EINVAL;
    }
 
    if (mach_info->default_display >= mach_info->num_displays) {
        dev_err(&pdev->dev, "default is %d but only %d displays\n",
                mach_info->default_display, mach_info->num_displays);
        return -EINVAL;
    }
 
    display = mach_info->displays + mach_info->default_display;
 
    irq = platform_get_irq(pdev, 0);
    if (irq < 0) {
        dev_err(&pdev->dev, "no irq for device\n");
        return -ENOENT;
    }
 
    fbinfo = framebuffer_alloc(sizeof(struct s3c2410fb_info), &pdev->dev);
    if (!fbinfo)
        return -ENOMEM;
 
    platform_set_drvdata(pdev, fbinfo);
 
    info = fbinfo->par;
    info->dev = &pdev->dev;
    info->drv_type = drv_type;
 
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (res == NULL) {
        dev_err(&pdev->dev, "failed to get memory registers\n");
        ret = -ENXIO;
        goto dealloc_fb;
    }
 
    size = (res->end - res->start) + 1;
    info->mem = request_mem_region(res->start, size, pdev->name);
    if (info->mem == NULL) {
        dev_err(&pdev->dev, "failed to get memory region\n");
        ret = -ENOENT;
        goto dealloc_fb;
    }
 
    info->io = ioremap(res->start, size);
    if (info->io == NULL) {
        dev_err(&pdev->dev, "ioremap() of registers failed\n");
        ret = -ENXIO;
        goto release_mem;
    }
 
    info->irq_base = info->io + ((drv_type == DRV_S3C2412) ? S3C2412_LCDINTBASE : S3C2410_LCDINTBASE);
 
    dprintk("devinit\n");
 
    strcpy(fbinfo->fix.id, driver_name);
 
    /* Stop the video */
    lcdcon1 = readl(info->io + S3C2410_LCDCON1);
    writel(lcdcon1 & ~S3C2410_LCDCON1_ENVID, info->io + S3C2410_LCDCON1);
 
    fbinfo->fix.type      = FB_TYPE_PACKED_PIXELS;
    fbinfo->fix.type_aux  = 0;
    fbinfo->fix.xpanstep  = 0;
    fbinfo->fix.ypanstep  = 0;
    fbinfo->fix.ywrapstep = 0;
    fbinfo->fix.accel     = FB_ACCEL_NONE;
 
    fbinfo->var.nonstd      = 0;
    fbinfo->var.activate    = FB_ACTIVATE_NOW;
    fbinfo->var.accel_flags = 0;
    fbinfo->var.vmode       = FB_VMODE_NONINTERLACED;
 
    fbinfo->fbops           = &s3c2410fb_ops;
    fbinfo->flags           = FBINFO_FLAG_DEFAULT;
    fbinfo->pseudo_palette  = &info->pseudo_pal;
 
    for (i = 0; i < 256; i++)
        info->palette_buffer[i] = PALETTE_BUFF_CLEAR;
 
    ret = request_irq(irq, s3c2410fb_irq, IRQF_DISABLED, pdev->name, info);
    if (ret) {
        dev_err(&pdev->dev, "cannot get irq %d - err %d\n", irq, ret);
        ret = -EBUSY;
        goto release_regs;
    }
 
    info->clk = clk_get(NULL, "lcd");
    if (!info->clk || IS_ERR(info->clk)) {
        printk(KERN_ERR "failed to get lcd clock source\n");
        ret = -ENOENT;
        goto release_irq;
    }
 
    clk_enable(info->clk);
    dprintk("got and enabled clock\n");
 
    msleep(1);
 
    info->clk_rate = clk_get_rate(info->clk);
 
    /* find maximum required memory size for display */
    for (i = 0; i < mach_info->num_displays; i++) {
        unsigned long smem_len = mach_info->displays[i].xres;
 
        smem_len *= mach_info->displays[i].yres;
        smem_len *= mach_info->displays[i].bpp;
        smem_len >>= 3;
        if (fbinfo->fix.smem_len < smem_len)
            fbinfo->fix.smem_len = smem_len;
    }
 
    /* Initialize video memory */
    ret = s3c2410fb_map_video_memory(fbinfo);
    if (ret) {
        printk(KERN_ERR "Failed to allocate video RAM: %d\n", ret);
        ret = -ENOMEM;
        goto release_clock;
    }
 
    dprintk("got video memory\n");
 
    fbinfo->var.xres = display->xres;
    fbinfo->var.yres = display->yres;
    fbinfo->var.bits_per_pixel = display->bpp;
 
    s3c2410fb_init_registers(fbinfo);
 
    s3c2410fb_check_var(&fbinfo->var, fbinfo);
 
    ret = s3c2410fb_cpufreq_register(info);
    if (ret < 0) {
        dev_err(&pdev->dev, "Failed to register cpufreq\n");
        goto free_video_memory;
    }
 
    ret = register_framebuffer(fbinfo);
    if (ret < 0) {
        printk(KERN_ERR "Failed to register framebuffer device: %d\n",
               ret);
        goto free_cpufreq;
    }
 
    /* create device files */
    ret = device_create_file(&pdev->dev, &dev_attr_debug);
    if (ret) {
        printk(KERN_ERR "failed to add debug attribute\n");
    }
 
    printk(KERN_INFO "fb%d: %s frame buffer device\n",
           fbinfo->node, fbinfo->fix.id);
 
    return 0;
 
free_cpufreq:
    s3c2410fb_cpufreq_deregister(info);
free_video_memory:
    s3c2410fb_unmap_video_memory(fbinfo);
release_clock:
    clk_disable(info->clk);
    clk_put(info->clk);
release_irq:
    free_irq(irq, info);
release_regs:
    iounmap(info->io);
release_mem:
    release_resource(info->mem);
    kfree(info->mem);
dealloc_fb:
    platform_set_drvdata(pdev, NULL);
    framebuffer_release(fbinfo);
    return ret;
}

Esqueça um pouco o que a função faz, e perceba como ela implementa o tratamento de erros. Para cada erro identificado, ela usa chamadas a goto para tratar o erro e fazer uma limpeza antes de retornar da função (desalocar buffers previamente alocados, liberar recursos, configurar portas de I/O, etc). São ao todo oito chamadas a goto apenas nesta função!

Não aprendemos que usar goto é uma técnica ruim? Também não aprendemos que todo código bem estruturado em C não deve ter nenhuma chamada a goto? Então como é possível encontrar um código assim dentro do kernel do Linux?

NÃO SE ASSUSTE

Usar goto em código C pode ser mais comum do que você imagina. Fiz um levantamento do uso da palavra-chave "goto" no kernel do Linux 2.6.37, no Busybox e no U-Boot.

Apenas o core do kernel (/kernel), possui 1.417 usos de goto. Quando incluimos os drivers de dispositivo (/drivers) e o código dependente de arquitetura (/arch) são 54.223 usos de goto. Fazendo a contagem completa, chegamos ao impressionante numero de 84.719 usos de goto no kernel do Linux!

Mas não é só o kernel do Linux que abusa de goto. O pacote Busybox, comum em sistemas embarcados com Linux, usa o goto 1.782 vezes. E o famoso bootloader U-Boot utiliza em 1.662 oportunidades.

Ok Sergio, então você esta me dizendo que estou livre para usar e abusar de goto no meu código?

Calma aí, não é bem assim!

BAIXANDO O NÍVEL

Veja o código que o compilador gcc gera em uma chamada a goto, comparando o fonte em C e o arquivo assembly gerado:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main()
{
    int i = 1;
 
    if (i) {
        printf("Goto error!\n");
        goto error;
    }
 
    printf("OK!\n");
 
    return 0;
 
error:
    return 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    .file   "goto.c"
    .section    .rodata
.LC0:
    .string "Goto error!"
.LC1:
    .string "OK!"
    .text
.globl main
    .type   main, @function
main:
    pushl   %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    subl    $32, %esp
    movl    $1, 28(%esp)
    cmpl    $0, 28(%esp)
    je  .L2
    movl    $.LC0, (%esp)
    call    puts
    nop
.L3:
    movl    $1, %eax
    jmp .L4
.L2:
    movl    $.LC1, (%esp)
    call    puts
    movl    $0, %eax
.L4:
    leave
    ret
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
    .section    .note.GNU-stack,"",@progbits

Veja que o if na linha 5 do fonte C gera uma chamada a "je .L2" (jump condicional) na linha 17 do código Assembly. E o goto na linha 7 do fonte C gera uma chamada a "jmp .L4" (jump) na linha 23 do código assembly.

Perceba que, na prática, tudo é tratado com uma instrução de salto no nível do assembly, não importa se você esta usando goto, if/else, switch, do/while ou for. Não existe nenhum impacto na performance ou no uso de memória da aplicação. Na verdade, em alguns casos, o uso de goto pode até melhorar a performance da aplicação. Mas normalmente seu uso é uma questão de legibilidade do código.

Os mais puristas afirmam que não existe nada que se escreva usando goto, que não possa ser escrito usando as estruturas de controle disponíveis na linguagem C (if/else, switch, do/while, for), e durante um bom tempo também acreditei nisso. Mas na prática, às vezes, a teoria é diferente! E existem alguns casos onde o uso de goto pode ser a melhor solução.

Sergio, você já não falou que é contra o uso de goto em código C? Sim, mas isso não muda o fato de que pode existir uma forma de utilizar esta palavra-chave melhorando a estrutura e a legibilidade do seu programa.

Mas você também não esta sempre defendendo qualidade de código, segurança, bla bla bla, e agora vem me falar que existe uma utilidade para o uso de "goto"? Sim, existe! É só olhar para os números (e para o código-fonte do kernel do Linux, e para outros tantos códigos open-source). Essa utilidade chama-se: Tratamento de Erros!

O PROBLEMA EM QUESTÃO

Uma das deficiências da linguagem C é a ausência de um mecanismo simples e eficiente de tratamento de erros. Já escrevi sobre isso neste post aqui.

O problema fica mais transparente quando precisamos lidar com limpeza (desalocação) de recursos. Dê uma olhada no exemplo abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int initRTC()
{
    char *buf1, *buf2;
 
    if ((buf1 = (char *)malloc(20)) == NULL)
        return ERR_RTC_MALLOC;
 
    if ((buf2 = (char *)malloc(40)) == NULL) {
        free(buf1);
        return ERR_RTC_MALLOC;
    }
 
    if (openRTC()) {
        free(buf2);
        free(buf1);
        return ERR_RTC_OPEN;
    }
 
    if (cfgRTC(buf1, 20, buf2, 40)) {
        closeRTC();
        free(buf2);
        free(buf1);
        return ERR_RTC_CFG;
    }
 
    if (txCmd(buf1, 20, buf2, 40)) {
        closeRTC();
        free(buf2);
        free(buf1);
        return ERR_RTC_TX;
    }
 
    closeRTC();
    free(buf2);
    free(buf1);
 
    return RTC_OK;
}

Esta função faz duas alocações de memória, abre a comunicação com o RTC, configura os buffers e transmite. Perceba que existe vários pontos de retorno.

Na linha 8, em caso de erro na chamada a malloc() para o buf2, é necessário liberar a memória alocada para buf1 antes de retornar. Da mesma forma, se der erro ao abrir a comunicação com o RTC na linha 13, é necessário liberar a memória alocada em buf1 e buf2. E por aí vai.

Para cada ponto de retorno adicional, precisamos lembrar de fazer a limpeza necessária antes de retornar. Veja que os trechos de código responsáveis pela limpeza são replicados nos vários pontos de retorno. Imagine o problema e os riscos envolvidos em dar manutenção num código desse tipo.

Veja agora a mesma função reescrita 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int initRTC()
{
    char *buf1, *buf2;
    int ret = RTC_OK;
 
    if ((buf1 = (char *)malloc(20)) == NULL) {
        ret = ERR_RTC_MALLOC;
        goto fim0;
    }
 
    if ((buf2 = (char *)malloc(40)) == NULL) {
        ret = ERR_RTC_MALLOC;
        goto fim1;
    }
 
    if (openRTC()) {
        ret = ERR_RTC_OPEN;
        goto fim2;
    }
 
    if (cfgRTC(buf1, 20, buf2, 40)) {
        ret = ERR_RTC_CFG;
        goto fim3;
    }
 
    if (txCmd(buf1, 20, buf2, 40)) {
        ret = ERR_RTC_TX;
        goto fim3;
    }
 
fim3:
    closeRTC();
fim2:
    free(buf2);
fim1:
    free(buf1);
fim0:
    return ret;
}

Agora a função tem apenas um ponto de retorno e não existe mais código replicado para fazer a limpeza (desalocação de recursos) em caso de erro, o que facilita a leitura e futuras manutenções. Me parece uma solução interessante para um problema comum e sem solução fácil em C.

É claro que existem outros mecanismos para resolver este mesmo problema. Você pode criar uma máquina de estados, conforme o exemplo abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
int initRTC()
{
    char *buf1, *buf2;
    int ret = RTC_OK, state = ST_BUF1;
 
    while (state != ST_DONE) {
 
        switch(state) {
 
            case ST_BUF1:
                if ((buf1 = (char *)malloc(20)) == NULL) {
                    ret = ERR_RTC_MALLOC;
                    state = ST_DONE;
                }
                else {
                    state = ST_BUF2;
                }
                break;
 
            case ST_BUF2:
                if ((buf2 = (char *)malloc(40)) == NULL) {
                    ret = ERR_RTC_MALLOC;
                    state = ST_ERR_BUF2;
                }
                else {
                    state = ST_OPEN_RTC;
                }
                break;
 
            case ST_OPEN_RTC:
                if (openRTC()) {
                    ret = ERR_RTC_OPEN;
                    state = ST_ERR_OPEN;
                }
                else {
                    state = ST_CFG_RTC;
                }
                break;
 
            // ....
 
            case ST_ERR_BUF2:
                free(buf1);
                state = ST_DONE;
                break;
 
            case ST_ERR_OPEN:
                free(buf2);
                state = ST_ERR_BUF2;
                break;
 
            case ST_ERR_TX:
                closeRTC();
                state = ST_ERR_OPEN;
                break;
 
            // ....
        }
    }
 
    return ret;
}

Não implementei todos os estados, mas dá para ter uma idéia do mecanismo utilizado. Perceba a complexidade deste código, comparado ao código com goto.

Podemos também aninhar chamadas if/else e controlar a desalocação de recursos com alguns flags:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int initRTC()
{
    char *buf1, *buf2;
    int ret = RTC_OK;
 
    if ((buf1 = (char *)malloc(20)) == NULL) {
        ret = ERR_RTC_MALLOC1;
    }
    else if ((buf2 = (char *)malloc(40)) == NULL) {
        ret = ERR_RTC_MALLOC2;
    }
    else if (openRTC()) {
        ret = ERR_RTC_OPEN;
    }
    else if (cfgRTC(buf1, 20, buf2, 40)) {
        ret = ERR_RTC_CFG;
    }
    else if (txCmd(buf1, 20, buf2, 40)) {
        ret = ERR_RTC_TX;
    }
 
    if (ret >= ERR_RTC_CFG) {
        closeRTC();
    }
 
    if (ret >= ERR_RTC_OPEN) {
        free(buf2);
    }
 
    if (ret >= ERR_RTC_MALLOC2) {
        free(buf1);
    }
 
    return ret;
}

Neste exemplo estamos usando a própria variável de retorno como flag para desalocação de recursos. O problema deste tipo de código é que nem sempre temos a possibilidade de aninhar if's e else's desta forma. Esta técnica pode também deixar o código muito sujo, dependendo da sua complexidade.

Ainda neste exemplo, você poderia usar o próprio estado do recurso para verificar se deve realizar a desalocação, implementando no fim da função:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    //...
 
    if (rtcOpenned) {
        closeRTC();
    }
 
    if (buf2 != NULL) {
        free(buf2);
    }
 
    if (buf1 != NULL) {
        free(buf1);
    }
 
    //...

Mas nem sempre você tem o controle de quais recursos precisa desalocar. De qualquer forma, o código com if/else aninhados ainda continua mais complicado de ler, quando comparado à solução com goto.

ONDE ESTA O PROBLEMA ENTÃO?

Se o código com goto neste caso é menos complicado de ler, e o binário gerado não sofre nenhum impacto na performance ou no uso de memória, porque não usá-lo?

Assim como grande parte dos problemas da vida, o erro esta no excesso. É muito fácil abusar deste recurso, e deixar o código cheio de "idas" e "vindas" (o famoso código-espaguete!).

Foi o que aconteceu no passado, e isso ajudou a criar a má reputação do uso de goto. Pense sempre nos prós e contras, reflita sobre sua forma de pensar e use os recursos disponíveis na linguagem com muita sabedoria.

Não foi meu objetivo neste artigo fazer qualquer tipo de apologia ao uso de "goto" em linguagem C, muito pelo contrário! Particularmente evito seu uso sempre que possível. Apenas acho válida uma análise mais profunda de nossos paradigmas de programação. Eu procuro sempre "guardar na manga" algumas técnicas para usar quando necessário.

E você? O que acha do uso de goto em linguagem C? Conhece alguma outra técnica para tratar o problema citado acima?

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.