O que é um "retorno de chamada" em C e como eles são implementados?

153

Pela leitura que fiz, o Core Audio depende muito de retornos de chamada (e C ++, mas isso é outra história).

Entendo o conceito (tipo de) de configurar uma função que é chamada por outra função repetidamente para realizar uma tarefa. Eu simplesmente não entendo como eles são configurados e como eles realmente funcionam. Quaisquer exemplos seriam apreciados.

noizetoys
fonte

Respostas:

203

Não há "retorno de chamada" em C - não mais do que qualquer outro conceito de programação genérico.

Eles são implementados usando ponteiros de função. Aqui está um exemplo:

void populate_array(int *array, size_t arraySize, int (*getNextValue)(void))
{
    for (size_t i=0; i<arraySize; i++)
        array[i] = getNextValue();
}

int getNextRandomValue(void)
{
    return rand();
}

int main(void)
{
    int myarray[10];
    populate_array(myarray, 10, getNextRandomValue);
    ...
}

Aqui, a populate_arrayfunção pega um ponteiro de função como terceiro parâmetro e o chama para obter os valores para preencher a matriz. Escrevemos o retorno de chamada getNextRandomValue, que retorna um valor aleatório, e passamos um ponteiro para ele populate_array. populate_arraychamará nossa função de retorno de chamada 10 vezes e atribuirá os valores retornados aos elementos na matriz especificada.

aib
fonte
2
Eu posso estar errado aqui, mas a linha em populate_array que chama o ponteiro da função não deve ser: array [i] = (* getNextValue) (); ?
Nathan Fellman
40
O operador de desreferência é opcional com ponteiros de função, assim como o endereço do operador. (f) (...) = (* meufunc) (...) e & meufunc =
meufunc
1
@ NathanFellman Acabei de ler a programação Expert C e explica bem o ponteiro de função.
Matt Clarkson
1
@ johnny Porque o padrão diz isso. Veja o comentário votado.
Aib
3
@Patrick: populateArray está em uma biblioteca (e foi escrita há 12 anos) e você escreveu você mesmo getNextRandomValue (ontem); portanto, não pode chamá-lo diretamente. Pense em uma função de classificação de biblioteca para a qual você mesmo fornece o comparador.
aib
121

Aqui está um exemplo de retornos de chamada em C.

Digamos que você queira escrever um código que permita o registro de retornos de chamada quando algum evento ocorrer.

Primeiro, defina o tipo de função usada para o retorno de chamada:

typedef void (*event_cb_t)(const struct event *evt, void *userdata);

Agora, defina uma função que é usada para registrar um retorno de chamada:

int event_cb_register(event_cb_t cb, void *userdata);

Este é o código que registra um retorno de chamada:

static void my_event_cb(const struct event *evt, void *data)
{
    /* do stuff and things with the event */
}

...
   event_cb_register(my_event_cb, &my_custom_data);
...

Nas partes internas do distribuidor de eventos, o retorno de chamada pode ser armazenado em uma estrutura que se parece com isso:

struct event_cb {
    event_cb_t cb;
    void *data;
};

É assim que o código executa um retorno de chamada.

struct event_cb *callback;

...

/* Get the event_cb that you want to execute */

callback->cb(event, callback->data);
Russell Bryant
fonte
Apenas o que eu precisava. A parte dos dados do usuário é muito útil se os usuários desejarem passar dados personalizados (por exemplo, identificadores de dispositivo) necessários na função de retorno de chamada.
Uceumern 30/07/2015
pergunta de verificação: O retorno de chamada é typedef com um asterisco porque é um ponteiro para o endereço da função? Se o asterisco estiver ausente, isso estaria incorreto? Se isso estiver incorreto, haverá duas estrelas ausentes na biblioteca libsrtp da cisco no github: github.com/cisco/libsrtp/blob/… github.com/cisco/libsrtp/blob/…
twildeman
@twildeman Parece trivial responder à sua própria pergunta, compilando no modo C padrão com avisos ativados. Você também pode escrever um programa de teste minimizado. Código como os libsrtpapresentados em não dá avisos. Presumo, então, que quando esse tipo aparece como argumento de função, é necessário 'decair' para um ponteiro para função, assim como matrizes decaem para ponteiros para seus primeiros elementos, então a mesma coisa acontece no final de qualquer jeito. Ele é interessante, porém, que as discussões de tais typedefs que eu encontrei nem sequer olhar para este aspecto, sim focar declarar protótipos ou ponteiros com ele
underscore_d
Não faço ideia do que isso faz e não pode ser compilado com êxito. Alguém pode explicar isso de maneira detalhada ou preencher o restante do código para compilar com êxito?
Andy Lin
20

Um simples programa de retorno de chamada. Espero que ele responda sua pergunta.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include "../../common_typedef.h"

typedef void (*call_back) (S32, S32);

void test_call_back(S32 a, S32 b)
{
    printf("In call back function, a:%d \t b:%d \n", a, b);
}

void call_callback_func(call_back back)
{
    S32 a = 5;
    S32 b = 7;

    back(a, b);
}

S32 main(S32 argc, S8 *argv[])
{
    S32 ret = SUCCESS;

    call_back back;

    back = test_call_back;

    call_callback_func(back);

    return ret;
}
Gautham Kantharaju
fonte
9

Uma função de retorno de chamada em C é equivalente a um parâmetro / variável de função atribuído a ser usado em outra função. Exemplo da Wiki

No código abaixo,

#include <stdio.h>
#include <stdlib.h>

/* The calling function takes a single callback as a parameter. */
void PrintTwoNumbers(int (*numberSource)(void)) {
    printf("%d and %d\n", numberSource(), numberSource());
}

/* A possible callback */
int overNineThousand(void) {
    return (rand() % 1000) + 9001;
}

/* Another possible callback. */
int meaningOfLife(void) {
    return 42;
}

/* Here we call PrintTwoNumbers() with three different callbacks. */
int main(void) {
    PrintTwoNumbers(&rand);
    PrintTwoNumbers(&overNineThousand);
    PrintTwoNumbers(&meaningOfLife);
    return 0;
}

A função (* numberSource) dentro da chamada de função PrintTwoNumbers é uma função para "retornar a chamada" / executar de dentro do PrintTwoNumbers conforme ditado pelo código enquanto é executado.

Portanto, se você tivesse algo como uma função pthread, poderia atribuir outra função para executar dentro do loop a partir da instanciação.

daemondave
fonte
6

Um retorno de chamada em C é uma função fornecida a outra função para "retornar a chamada" em algum momento em que a outra função está executando sua tarefa.

duas maneiras pelas quais um retorno de chamada é usado : retorno de chamada síncrono e retorno de chamada assíncrono. Um retorno de chamada síncrono é fornecido para outra função que realiza alguma tarefa e depois retorna ao chamador com a tarefa concluída. Um retorno de chamada assíncrono é fornecido para outra função que iniciará uma tarefa e retornará ao chamador com a tarefa possivelmente não concluída.

Um retorno de chamada síncrono é normalmente usado para fornecer um delegado para outra função na qual a outra função delega alguma etapa da tarefa. Exemplos clássicos dessa delegação são as funções bsearch()e qsort()da Biblioteca Padrão C. Ambas as funções recebem um retorno de chamada que é usado durante a tarefa que a função está fornecendo, para que o tipo de dados pesquisado, no caso de bsearch(), ou classificado, no caso de qsort(), não precise ser conhecido pela função que está sendo usava.

Por exemplo, aqui está um pequeno programa de amostra com o bsearch()uso de diferentes funções de comparação, retornos de chamada síncronos. Ao nos permitir delegar a comparação de dados em uma função de retorno de chamada, a bsearch()função nos permite decidir em tempo de execução que tipo de comparação queremos usar. Isso é síncrono porque, quando a bsearch()função retorna, a tarefa é concluída.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    int iValue;
    int kValue;
    char label[6];
} MyData;

int cmpMyData_iValue (MyData *item1, MyData *item2)
{
    if (item1->iValue < item2->iValue) return -1;
    if (item1->iValue > item2->iValue) return 1;
    return 0;
}

int cmpMyData_kValue (MyData *item1, MyData *item2)
{
    if (item1->kValue < item2->kValue) return -1;
    if (item1->kValue > item2->kValue) return 1;
    return 0;
}

int cmpMyData_label (MyData *item1, MyData *item2)
{
    return strcmp (item1->label, item2->label);
}

void bsearch_results (MyData *srch, MyData *found)
{
        if (found) {
            printf ("found - iValue = %d, kValue = %d, label = %s\n", found->iValue, found->kValue, found->label);
        } else {
            printf ("item not found, iValue = %d, kValue = %d, label = %s\n", srch->iValue, srch->kValue, srch->label);
        }
}

int main ()
{
    MyData dataList[256] = {0};

    {
        int i;
        for (i = 0; i < 20; i++) {
            dataList[i].iValue = i + 100;
            dataList[i].kValue = i + 1000;
            sprintf (dataList[i].label, "%2.2d", i + 10);
        }
    }

//  ... some code then we do a search
    {
        MyData srchItem = { 105, 1018, "13"};
        MyData *foundItem = bsearch (&srchItem, dataList, 20, sizeof(MyData), cmpMyData_iValue );

        bsearch_results (&srchItem, foundItem);

        foundItem = bsearch (&srchItem, dataList, 20, sizeof(MyData), cmpMyData_kValue );
        bsearch_results (&srchItem, foundItem);

        foundItem = bsearch (&srchItem, dataList, 20, sizeof(MyData), cmpMyData_label );
        bsearch_results (&srchItem, foundItem);
    }
}

Um retorno de chamada assíncrono é diferente, pois quando a função chamada à qual fornecemos um retorno de chamada retorna, a tarefa pode não ser concluída. Esse tipo de retorno de chamada é frequentemente usado com E / S assíncrona na qual uma operação de E / S é iniciada e, quando concluída, o retorno de chamada é invocado.

No programa a seguir, criamos um soquete para escutar solicitações de conexão TCP e, quando uma solicitação é recebida, a função que escuta chama a função de retorno de chamada fornecida. Esse aplicativo simples pode ser exercitado executando-o em uma janela enquanto você usa o telnetutilitário ou um navegador da Web para tentar se conectar em outra janela.

Tirei a maior parte do código WinSock do exemplo que a Microsoft fornece com a accept()função em https://msdn.microsoft.com/en-us/library/windows/desktop/ms737526(v=vs.85).aspx

Esse aplicativo inicia um listen()host local, 127.0.0.1, usando a porta 8282, para que você possa usar um telnet 127.0.0.1 8282ou outro http://127.0.0.1:8282/.

Este aplicativo de exemplo foi criado como um aplicativo de console com o Visual Studio 2017 Community Edition e está usando a versão dos soquetes do Microsoft WinSock. Para um aplicativo Linux, as funções WinSock precisariam ser substituídas pelas alternativas Linux e a biblioteca de threads do Windows usaria pthreads.

#include <stdio.h>
#include <winsock2.h>
#include <stdlib.h>
#include <string.h>

#include <Windows.h>

// Need to link with Ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

// function for the thread we are going to start up with _beginthreadex().
// this function/thread will create a listen server waiting for a TCP
// connection request to come into the designated port.
// _stdcall modifier required by _beginthreadex().
int _stdcall ioThread(void (*pOutput)())
{
    //----------------------
    // Initialize Winsock.
    WSADATA wsaData;
    int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iResult != NO_ERROR) {
        printf("WSAStartup failed with error: %ld\n", iResult);
        return 1;
    }
    //----------------------
    // Create a SOCKET for listening for
    // incoming connection requests.
    SOCKET ListenSocket;
    ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (ListenSocket == INVALID_SOCKET) {
        wprintf(L"socket failed with error: %ld\n", WSAGetLastError());
        WSACleanup();
        return 1;
    }
    //----------------------
    // The sockaddr_in structure specifies the address family,
    // IP address, and port for the socket that is being bound.
    struct sockaddr_in service;
    service.sin_family = AF_INET;
    service.sin_addr.s_addr = inet_addr("127.0.0.1");
    service.sin_port = htons(8282);

    if (bind(ListenSocket, (SOCKADDR *)& service, sizeof(service)) == SOCKET_ERROR) {
        printf("bind failed with error: %ld\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }
    //----------------------
    // Listen for incoming connection requests.
    // on the created socket
    if (listen(ListenSocket, 1) == SOCKET_ERROR) {
        printf("listen failed with error: %ld\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }
    //----------------------
    // Create a SOCKET for accepting incoming requests.
    SOCKET AcceptSocket;
    printf("Waiting for client to connect...\n");

    //----------------------
    // Accept the connection.
    AcceptSocket = accept(ListenSocket, NULL, NULL);
    if (AcceptSocket == INVALID_SOCKET) {
        printf("accept failed with error: %ld\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }
    else
        pOutput ();   // we have a connection request so do the callback

    // No longer need server socket
    closesocket(ListenSocket);

    WSACleanup();
    return 0;
}

// our callback which is invoked whenever a connection is made.
void printOut(void)
{
    printf("connection received.\n");
}

#include <process.h>

int main()
{
     // start up our listen server and provide a callback
    _beginthreadex(NULL, 0, ioThread, printOut, 0, NULL);
    // do other things while waiting for a connection. In this case
    // just sleep for a while.
    Sleep(30000);
}
Richard Chambers
fonte
Excelente resposta, mostrando retornos de chamada síncronos e assíncronos. Outro exemplo concreto do uso de retornos de chamada assíncronos no C- * NIX são sinais assíncronos e seus manipuladores de sinais. Aqui está uma excelente descrição de como os manipuladores de sinal são processados ​​no Linux [link] ( stackoverflow.com/questions/6949025/… ).
drlolly
4

Os retornos de chamada em C são geralmente implementados usando ponteiros de função e um ponteiro de dados associado. Você passa sua função on_event()e ponteiros de dados para uma função de estrutura watch_events()(por exemplo). Quando um evento acontece, sua função é chamada com seus dados e alguns dados específicos do evento.

Os retornos de chamada também são usados ​​na programação da GUI. O tutorial do GTK + tem uma boa seção sobre a teoria dos sinais e retornos de chamada .

John Millikin
fonte
2

Este artigo da Wikipedia tem um exemplo em C.

Um bom exemplo é que novos módulos escritos para aumentar o servidor da Web Apache se registram no processo principal do apache, passando os ponteiros de função para que essas funções sejam chamadas de volta ao processo de solicitações de páginas da web.

Leonard
fonte
0

Geralmente, isso pode ser feito usando um ponteiro de função, que é uma variável especial que aponta para o local da memória de uma função. Você pode usar isso para chamar a função com argumentos específicos. Portanto, provavelmente haverá uma função que define a função de retorno de chamada. Isso aceitará um ponteiro de função e armazenará esse endereço em algum lugar onde ele possa ser usado. Depois disso, quando o evento especificado for acionado, ele chamará essa função.

MDEC
fonte
0

É muito mais fácil entender uma idéia através do exemplo. O que foi dito sobre a função de retorno de chamada em C até agora são ótimas respostas, mas provavelmente o maior benefício do uso do recurso é manter o código limpo e organizado.

Exemplo

O código C a seguir implementa a classificação rápida. A linha mais interessante no código abaixo é esta, onde podemos ver a função de retorno de chamada em ação:

qsort(arr,N,sizeof(int),compare_s2b);

O compare_s2b é o nome da função que qsort () está usando para chamar a função. Isso mantém o qsort () tão organizado (portanto, mais fácil de manter). Você apenas chama uma função pelo nome de dentro de outra função (é claro, a declaração do protótipo da função, pelo menos, deve preceder antes que possa ser chamada de outra função).

O Código Completo

#include <stdio.h>
#include <stdlib.h>

int arr[]={56,90,45,1234,12,3,7,18};
//function prototype declaration 

int compare_s2b(const void *a,const void *b);

int compare_b2s(const void *a,const void *b);

//arranges the array number from the smallest to the biggest
int compare_s2b(const void* a, const void* b)
{
    const int* p=(const int*)a;
    const int* q=(const int*)b;

    return *p-*q;
}

//arranges the array number from the biggest to the smallest
int compare_b2s(const void* a, const void* b)
{
    const int* p=(const int*)a;
    const int* q=(const int*)b;

    return *q-*p;
}

int main()
{
    printf("Before sorting\n\n");

    int N=sizeof(arr)/sizeof(int);

    for(int i=0;i<N;i++)
    {
        printf("%d\t",arr[i]);
    }

    printf("\n");

    qsort(arr,N,sizeof(int),compare_s2b);

    printf("\nSorted small to big\n\n");

    for(int j=0;j<N;j++)
    {
        printf("%d\t",arr[j]);
    }

    qsort(arr,N,sizeof(int),compare_b2s);

    printf("\nSorted big to small\n\n");

    for(int j=0;j<N;j++)
    {
        printf("%d\t",arr[j]);
    }

    exit(0);
}
Até parece
fonte