setjmp e longjmp
LidoNesta aula, exploramos as funções setjmp e longjmp da biblioteca padrão C, que permitem saltos não locais entre funções. Discutimos seus casos de uso raros, como recuperação de erros em sistemas embarcados ou implementação de corrotinas, e os riscos associados, incluindo problemas com variáveis voláteis e vazamentos de recursos. Também comparamos essa técnica com o tratamento de erro tradicional baseado em códigos de retorno.
Bem-vindo à aula 85 do nosso curso de Linguagem C. Hoje vamos mergulhar em um tópico avançado e pouco utilizado: as funções setjmp e longjmp. Essas funções fazem parte da biblioteca padrão C (definidas em <setjmp.h>) e permitem que um programa realize saltos não locais, ou seja, transferir o fluxo de execução de um ponto do programa para outro, ignorando a hierarquia normal de chamadas de função. Enquanto goto permite saltos dentro da mesma função, setjmp e longjmp podem saltar entre funções, o que pode ser útil em situações específicas, mas também introduz uma série de riscos e complexidades.
Antes de prosseguir, é importante entender que este é um recurso poderoso, mas perigoso. Seu uso é raro e deve ser feito com muita cautela. Vamos analisar como funcionam, quando faz sentido usá-las, quais são os riscos e como se comparam ao tratamento de erro convencional em C.
Saltos não locais
Em C, o fluxo de execução normalmente segue a pilha de chamadas: uma função chama outra, que retorna ao final, e assim por diante. setjmp e longjmp quebram esse fluxo. A função setjmp salva o contexto atual do programa (registradores, ponteiro de pilha, etc.) em uma variável do tipo jmp_buf. Quando chamamos longjmp com essa mesma variável, o programa restaura o contexto salvo e a execução retorna como se setjmp tivesse acabado de retornar um valor específico.
O protótipo é:
#include <setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
Quando setjmp é chamada pela primeira vez, ela retorna 0. Se longjmp for chamada mais tarde, a execução retorna ao ponto do setjmp e o valor de retorno é o valor passado como segundo argumento para longjmp (que deve ser diferente de 0, pois 0 é reservado para a chamada inicial).
Vamos ver um exemplo básico:
#include <stdio.h>
#include <setjmp.h>
jmp_buf env;
void func(void) {
printf("Dentro de func\n");
longjmp(env, 42);
printf("Esta linha não será executada\n");
}
int main(void) {
if (setjmp(env) == 0) {
printf("Primeira chamada de setjmp\n");
func();
} else {
printf("Retornou de longjmp com valor %d\n", setjmp(env)); // na verdade, o valor de retorno é 42, mas não podemos chamar setjmp de novo assim
}
return 0;
}
No código acima, setjmp é chamada no main. Ela retorna 0, então entramos no primeiro bloco. Chamamos func, que imprime uma mensagem e chama longjmp. Isso faz com que a execução volte ao ponto do setjmp, e desta vez ela retorna 42. O programa então cai no bloco else (mas note que o exemplo acima está incorreto, pois chama setjmp de novo no else; na prática, devemos guardar o valor de retorno em uma variável).
Um exemplo mais correto:
#include <stdio.h>
#include <setjmp.h>
jmp_buf env;
void func(void) {
printf("Dentro de func\n");
longjmp(env, 42);
printf("Esta linha não será executada\n");
}
int main(void) {
int ret = setjmp(env);
if (ret == 0) {
printf("Primeira chamada de setjmp\n");
func();
} else {
printf("Retornou de longjmp com valor %d\n", ret);
}
return 0;
}
Ao executar, a saída será:
Primeira chamada de setjmp
Dentro de func
Retornou de longjmp com valor 42
Repare que a linha printf("Esta linha não será executada\n"); não aparece, pois o fluxo foi interrompido.
Esse mecanismo é chamado de salto não local porque o salto não respeita a hierarquia de chamadas: ele pode pular de uma função para outra, sem que as funções intermediárias retornem normalmente.
Casos de uso (raros)
Devido à sua natureza não estruturada, setjmp e longjmp são raramente usados. No entanto, existem alguns cenários em que eles podem ser úteis:
- Tratamento de erros em sistemas embarcados: Em sistemas com restrições de memória ou tempo real, pode ser necessário lidar com erros fatais sem usar exceções (que não existem em C).
setjmp/longjmppodem ser usados para sair de uma condição de erro profunda e voltar a um ponto seguro, como o loop principal. - Corrotinas (cooperativas): Em implementações de corrotinas, é possível usar
setjmpelongjmppara alternar entre contextos de execução. Cada corrotina pode ter seu própriojmp_bufe, ao chamarlongjmp, o controle é transferido para outra corrotina. - Interrupção de processamento: Em bibliotecas de processamento de sinais, pode-se usar
longjmppara sair de um manipulador de sinal e retornar a um ponto seguro, mas isso é extremamente arriscado devido à reentrância. - Parsing e análise sintática: Em algumas implementações de parsers, quando um erro é encontrado em uma sub-rotina profunda,
longjmppode ser usado para pular de volta ao nível superior de análise, em vez de propagar códigos de erro manualmente.
Um exemplo de uso em corrotina (simplificado):
#include <stdio.h>
#include <setjmp.h>
jmp_buf coroutine1, coroutine2;
void task1(void) {
while (1) {
printf("Tarefa 1 executando\n");
if (setjmp(coroutine1) == 0) {
longjmp(coroutine2, 1);
}
}
}
void task2(void) {
while (1) {
printf("Tarefa 2 executando\n");
if (setjmp(coroutine2) == 0) {
longjmp(coroutine1, 1);
}
}
}
int main(void) {
// Inicializa as corrotinas
if (setjmp(coroutine1) == 0) {
task1();
}
if (setjmp(coroutine2) == 0) {
task2();
}
return 0;
}
Esse exemplo é apenas ilustrativo e tem problemas de lógica, mas mostra a ideia de alternância.
Riscos
O uso de setjmp e longjmp é repleto de riscos e armadilhas. Alguns dos principais são:
- Vazamento de recursos: Se uma função aloca memória (com
malloc) ou abre arquivos (comfopen) e depois chamalongjmp, esses recursos não são liberados, pois as funções de limpeza não são executadas. Isso pode levar a vazamentos de memória ou arquivos abertos indefinidamente. - Variáveis locais não voláteis: Após um
longjmp, o valor de variáveis locais automáticas que foram modificadas entre osetjmpe olongjmpé indeterminado, a menos que sejam declaradas comovolatile. Isso ocorre porque o compilador pode otimizar o código assumindo que o fluxo de execução é linear. A norma C diz que somente variáveisvolatileou estáticas têm seus valores preservados. - Ponteiros inválidos: Se um ponteiro para uma estrutura de dados local for usado após o
longjmp, a memória pode não ser mais válida, pois a pilha foi restaurada. - Comportamento indefinido em certas situações: Chamar
longjmpa partir de um manipulador de sinal pode ter comportamento indefinido, a menos que o sinal seja assíncrono e não interrompa uma função não reentrante. - Dificuldade de depuração: O fluxo de execução não é linear, o que torna o código difícil de entender e depurar. Ferramentas de análise estática podem não detectar problemas.
Exemplo de problema com variáveis:
#include <stdio.h>
#include <setjmp.h>
jmp_buf env;
void func(void) {
int local = 10; // não volátil
printf("Valor local antes: %d\n", local);
longjmp(env, 1);
}
int main(void) {
int ret = setjmp(env);
if (ret == 0) {
func();
} else {
// local não está mais em escopo, mas se acessássemos de alguma forma, seria indefinido
printf("Após longjmp\n");
}
return 0;
}
Neste exemplo, a variável local não é acessada após o longjmp, mas se fosse, seu valor seria imprevisível.
vs tratamento de erro normal
Em C, a maneira tradicional de tratar erros é através de códigos de retorno, ponteiros de erro ou variáveis globais de erro (como errno). Essa abordagem é estruturada e previsível. Vamos comparar setjmp/longjmp com o tratamento de erro convencional:
- Estruturação: O tratamento de erro normal mantém um fluxo linear, o que é mais fácil de entender e manter.
longjmpquebra a estrutura, tornando o código menos legível. - Segurança: Com códigos de retorno, o programador é forçado a verificar e propagar erros, o que pode ser tedioso, mas é seguro.
longjmppode ignorar a necessidade de limpeza, causando vazamentos. - Desempenho: Em alguns casos,
longjmppode ser mais rápido do que propagar códigos de erro através de muitas camadas de funções, pois evita múltiplas verificações. No entanto, essa diferença raramente é significativa. - Portabilidade:
setjmpelongjmpsão padrão C, mas seu comportamento exato pode variar entre plataformas, especialmente em relação a otimizações. - Uso em C++: Em C++,
setjmp/longjmpnão interagem bem com objetos que têm destrutores, pois o desenrolamento da pilha não ocorre. Isso torna seu uso ainda mais arriscado.
Um exemplo de tratamento de erro normal:
#include <stdio.h>
#include <stdlib.h>
int process_file(const char *filename) {
FILE *f = fopen(filename, "r");
if (!f) {
return -1; // erro
}
// ... processa
fclose(f);
return 0;
}
int main(void) {
if (process_file("dados.txt") != 0) {
fprintf(stderr, "Erro ao processar arquivo\n");
return 1;
}
printf("Sucesso\n");
return 0;
}
Com longjmp, o mesmo poderia ser feito, mas seria necessário garantir que o arquivo fosse fechado antes do salto, o que é difícil se o erro ocorrer em uma função aninhada.
Boas práticas e observações finais
Se você decidir usar setjmp e longjmp, siga estas recomendações:
- Use-os apenas em casos extremamente específicos, como corrotinas ou sistemas embarcados com requisitos rígidos.
- Declare todas as variáveis locais que precisam ser preservadas como
volatile. - Certifique-se de que todos os recursos (memória, arquivos, etc.) sejam liberados antes de chamar
longjmp, ou faça a limpeza no ponto de retorno. - Evite usar
longjmpdentro de manipuladores de sinais, a menos que você saiba exatamente o que está fazendo. - Documente claramente o fluxo de controle no código para que outros programadores possam entender.
- Considere alternativas, como máquinas de estado ou funções de retorno de erro, antes de optar por
longjmp.
Em resumo, setjmp e longjmp são ferramentas poderosas, mas perigosas. Use-as com moderação e sempre com plena consciência dos riscos.
Referências
- cppreference: setjmp
- cppreference: longjmp
- Linux man page: setjmp(3)
- GNU C Library Manual: Non-Local Exits
- Wikipedia: setjmp.h
- POSIX: setjmp
Exercícios
-
Explique a diferença entre
gotoesetjmp/longjmp.✓ Resposta:gotosó pode pular para um rótulo dentro da mesma função, enquantosetjmp/longjmppodem pular entre funções, pois salvam e restauram o contexto de execução.gotoé um salto local,longjmpé um salto não local. -
Escreva um programa que use
setjmpelongjmppara simular o tratamento de uma divisão por zero, pulando de uma função que tenta a divisão para omain.✓ Resposta:#include <stdio.h> #include <setjmp.h> jmp_buf env; void divide(int a, int b) { if (b == 0) { longjmp(env, 1); } printf("%d / %d = %d\n", a, b, a / b); } int main(void) { int ret = setjmp(env); if (ret == 0) { divide(10, 0); } else { printf("Erro: divisão por zero!\n"); } return 0; } -
Quais são os principais riscos de usar
longjmpem relação a recursos como memória e arquivos? Dê um exemplo de código que cause vazamento de recurso.✓ Resposta: O principal risco é quelongjmpnão executa nenhuma limpeza automática. Se uma função alocar memória ou abrir um arquivo e depois chamarlongjmp, esses recursos não são liberados. Exemplo:#include <stdio.h> #include <stdlib.h> #include <setjmp.h> jmp_buf env; void func(void) { char *p = malloc(100); FILE *f = fopen("teste.txt", "w"); longjmp(env, 1); // vaza p e f } int main(void) { if (setjmp(env) == 0) { func(); } return 0; } -
Explique por que variáveis locais não voláteis podem ter valores indefinidos após um
longjmpe como declarar uma variável comovolatileresolve o problema.✓ Resposta: O compilador pode otimizar o código assumindo que o fluxo de execução é linear e que variáveis locais não mudam de forma inesperada. Quandolongjmprestaura o contexto, a pilha é revertida, mas as variáveis locais que foram modificadas podem não ter seus valores atualizados, porque o compilador pode ter mantido cópias em registradores. Declarar uma variável comovolatileforça o compilador a reler a variável da memória toda vez que ela for acessada, garantindo que o valor atual seja usado. -
Em que situações você consideraria usar
setjmp/longjmpem vez de códigos de retorno? Justifique.✓ Resposta: Usaria em sistemas embarcados com restrições de tempo real, onde o custo de propagar códigos de erro através de muitas camadas é inaceitável, ou na implementação de corrotinas, onde a alternância de contexto é necessária. Também em parsers complexos onde erros podem ocorrer em profundidade e o tratamento linear seria muito verboso. No entanto, sempre avaliaria se a complexidade adicional vale a pena.