Você usa goto nos seus códigos em C?
- por Sergio Prado
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