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/longjmp podem 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 setjmp e longjmp para alternar entre contextos de execução. Cada corrotina pode ter seu próprio jmp_buf e, ao chamar longjmp, o controle é transferido para outra corrotina.
  • Interrupção de processamento: Em bibliotecas de processamento de sinais, pode-se usar longjmp para 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, longjmp pode 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 (com fopen) e depois chama longjmp, 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 o setjmp e o longjmp é indeterminado, a menos que sejam declaradas como volatile. 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áveis volatile ou 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 longjmp a 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. longjmp quebra 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. longjmp pode ignorar a necessidade de limpeza, causando vazamentos.
  • Desempenho: Em alguns casos, longjmp pode 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: setjmp e longjmp sã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/longjmp nã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 longjmp dentro 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

Exercícios

  1. Explique a diferença entre goto e setjmp/longjmp.

    ✓ Resposta: goto só pode pular para um rótulo dentro da mesma função, enquanto setjmp/longjmp podem pular entre funções, pois salvam e restauram o contexto de execução. goto é um salto local, longjmp é um salto não local.
  2. Escreva um programa que use setjmp e longjmp para simular o tratamento de uma divisão por zero, pulando de uma função que tenta a divisão para o main.

    ✓ 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;
    }
    
  3. Quais são os principais riscos de usar longjmp em relação a recursos como memória e arquivos? Dê um exemplo de código que cause vazamento de recurso.

    ✓ Resposta: O principal risco é que longjmp não executa nenhuma limpeza automática. Se uma função alocar memória ou abrir um arquivo e depois chamar longjmp, 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;
    }
    
  4. Explique por que variáveis locais não voláteis podem ter valores indefinidos após um longjmp e como declarar uma variável como volatile resolve 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. Quando longjmp restaura 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 como volatile força o compilador a reler a variável da memória toda vez que ela for acessada, garantindo que o valor atual seja usado.
  5. Em que situações você consideraria usar setjmp/longjmp em 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.