Última atualização:
Linguagem C: callbacks
Linguagem C: callbacks

Linguagem C – Como escrever callbacks?

Nawarian
Nawarian C

De algumas décadas pra cá se popularizou a utilização de callbacks, funções que são referenciadas por outras funções e executadas de forma não deterministica – principalmente por causa do paradigma assíncrono de programação.

A linguagem C já oferece suporte a callbacks há muito tempo, mas sua utilização não é parecida com linguagens mais populares como o JavaScript ou PHP. Callbacks em C precisam ter tipos definidos em tempo de compilação e são tratados como ponteiros. Vem comigo que eu te explico!

Exemplo de callback e utilização de callbacks em C

Se você não quiser entender como tudo funciona e quiser pular para a solução, aqui está!

// Define a assinatura do callback
typedef void (*hello_t)(const char *name);

// Implementação que será utilizada como callback
void say_hello(const char *name)
{
    printf("Olá, %s!\n", name);
}

int main(void)
{
    // Variável que armazena o callback
    hello_t callback = say_hello;
    
    // Como chamar o callback
    callback("codamos.com.br"); // equivalente a say_hello("codamos.com.br")

    return 0;
}

Para entender bem o que está acontecendo no exemplo acima, continue lendo que eu te explico 😉

Como definir um callback em C?

Antes de entender como escrever um callback em C, você precisa entender como funcionam ponteiros em C. Sugiro a leitura do material de aula do professor Márcio Sarroglia Pinho (PUCRS) sobre ponteiros, após ler este material você provavelmente vai entender bem como ponteiros funcionam!

Ponteiros em C apenas referenciam um endereço na memória. Então se todo o nosso código é carregado em memória provavelmente poderíamos referenciar uma função, certo? Corretíssimo! E é assim que podemos trabalhar com callbacks. Vejamos o exemplo abaixo:

void say_hello(void)
{
  // ...
}

int main(void)
{
  void *callback = say_hello;
  // callback agora aponta para o endereço de (void say_hello(void))

  return 0;
}

No exemplo acima a variável callback é definida como *callback, que é como definimos um ponteiro. O tipo deste ponteiro é void, o que significa dizer que a linguagem C não precisa saber nada sobre o que está contido naquele endereço de memória.

Porém não é possível executar a função say_hello() através da variável callback sem indicar ao compilador com quais tipos estamos lidando. A forma mais clara de indicar os tipos é através da estrutura typedef, como a seguir:

// Define um tipo "callback_t" que é uma função
typedef void (*callback_t)(void);

void say_hello(void)
{
  // ...
}

int main(void)
{
  callback_t callback = say_hello;
  
  callback();
  return 0;
}

No exemplo acima nós definimos um tipo chamado callback_t que é um ponteiro de função. Este ponteiro indica que a função retorna void e que não possui parâmetro algum.

Supondo que a função say_hello() recebesse um parâmetro do tipo string, poderíamos mudar o código para o seguinte:

// Define um tipo "callback_t" que é uma função
typedef void (*callback_t)(const char *name);

void say_hello(const char *name)
{
  // ...
}

int main(void)
{
  callback_t callback = say_hello;
  
  callback("codamos.com.br");
  return 0;
}

Repare como a definição do tipo callback_t precisou se adaptar para receber const char *name como parâmetro. Do contrário a chamada callback("codamos.com.br") geraria um erro.

Portanto para definir um callback você pode utilizar a sintaxe abaixo:

typedef <tipo_de_retorno> (*nome_do_callback)(<tipo> param1, <tipo> param2, ...);

Como passar e receber um callback por parâmetro em C?

Receber um callback por parâmetro pode ser feito de duas formas diferentes: através da definição de tipos (typedef) ou através de uma definição na assinatura da função.

Para receber um callback via parâmetro utilizando a sua definição typedef basta referenciar o tipo, como no exemplo abaixo:

typedef void (*callback_t)(const char *name);

void run_callback(callback_t callback)
{
  callback("codamos.com.br");
}

Você também pode descrever a assinatura do callback na assinatura da função, sem utilizar typedef como no exemplo abaixo:

void run_callback(void (*callback)(const char *name))
{
  callback("codamos.com.br");
}

A segunda opção, apesar de não definir um novo tipo, é muito mais difícil de ler. Portanto eu não recomendo a sua utilização.

Quando utilizar um callback em C?

Callbacks podem ser utilizados em diferentes situações, abaixo vou listar apenas algumas delas.

Programação assíncrona

O estilo assíncrono de programação, que ficou popular com a utilização de JavaScript em ambientes de navegador e também no NodeJS, depende da utilização de callbacks para delegar a execução de funções para um futuro indeterminado.

Por exemplo, a implementação do Event Loop utilizado pelo NodeJS depende da biblioteca libuv escrita em C/C++. Esta implementação permite realizar operações de entrada e saída como ler o disco rígido ou realizar escritas ou leituras por rede de forma não bloqueante e organizar o fluxo de execução através de callbacks.

Um outro exemplo de programação assíncrona que utiliza callbacks em C é o multithreading, a capacidade de executar mais de um (sub) processo ao mesmo tempo. Em C uma implementação muito conhecida é a da biblioteca pthreads que recebe um callback como parâmetro da função pthread_create().

Programação de rede e IoT

Normalmente utilizamos callbacks sempre que precisamos delegar uma tarefa a outro processo: uma thread, o sistema operacional ou mesmo um evento externo. É muito comum, por exemplo, utilizar callbacks para tratar eventos de rede ou de sensores. Veja o exemplo (não realista) abaixo:

void handle_message(connection_t conn, const char *msg, size_t len);

void on_connect_callback(connection_t conn)
{
  on_message(conn, handle_message);
}

socket_t socket = bind_socket("0.0.0.0", 8080);
on_connect(socket, on_connect_callback);

No exemplo acima nós abrimos um socket que permite que diferentes computadores se conectem a este programa através da porta 8080. A cada nova conexão o programa irá chamar a função on_connect_callback(). E nesta função registramos também outro callback, o handle_message(), que deverá ser chamado a cada vez que aquela conexão enviar algum pacote de rede para o nosso programa.

Como são ações externas nós não podemos controlá-las e, portanto, o callback é justamente uma ferramenta para organizar o fluxo do nosso código em situações assíncronas em que não controlamos o fluxo da aplicação.

Para trabalhar com eventos

Callbacks não precisam ser utilizados apenas em situações em que trabalhamos com redes, disco ou sensores de forma assíncrona e/ou não-bloqueante.

Em muitos casos a utilização de eventos nos ajuda a organizar o nosso código melhor. Por exemplo, no código abaixo nós lemos eventos de um arquivo de texto e executamos seus respectivos callbacks:

typedef void (*callback_t)(void);

void replay_events(callback_t user_created, callback_t user_updated)
// Ler eventos a partir de um arquivo de texto
int events[];
int total_events;

for (int i = 0; i < total_events; ++i) {
  switch (events[i]) {
    case EV_USER_CREATED:
      user_created();
      break;
    case EV_USER_UPDATED:
      user_updated();
      break;
    // ...
  }
}

Da mesma forma como passamos cada callback via parâmetro, poderíamos tê-los passado via HashMap, que é uma estrutura muito mais dinâmica e nos permitiria registrar diversos callbacks para diferentes eventos.

Conclusão

Neste artigo vimos como e quando implementar um callback utilizando a linguagem C, quais suas utilidades e diferentes casos de uso. Também vimos por cima alguns conceitos importantes como os de programação assíncrona e ponteiros.

Não deixe de compartilhar com seus colegas e seguidores caso tenha gostado deste artigo ou ache que pode ser útil para outras pessoas.

Até a próxima! 👋

Comentários