Linguagem C – Como escrever callbacks?
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