¿Qué es una "devolución de llamada" en C y cómo se implementan?


153

De la lectura que he hecho, Core Audio depende en gran medida de las devoluciones de llamada (y C ++, pero esa es otra historia).

Entiendo el concepto (más o menos) de configurar una función que otra función llama repetidamente para realizar una tarea. Simplemente no entiendo cómo se configuran y cómo funcionan realmente. Cualquier ejemplo sería apreciado.

Respuestas:


203

No hay "devolución de llamada" en C, no más que cualquier otro concepto de programación genérico.

Se implementan utilizando punteros de función. Aquí hay un ejemplo:

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);
    ...
}

Aquí, la populate_arrayfunción toma un puntero de función como su tercer parámetro y lo llama para obtener los valores con los que llenar la matriz. Hemos escrito la devolución de llamada getNextRandomValue, que devuelve un valor aleatorio, y le hemos pasado un puntero populate_array. populate_arrayllamará a nuestra función de devolución de llamada 10 veces y asignará los valores devueltos a los elementos en la matriz dada.


2
Puedo estar equivocado aquí, pero no debería la línea en populate_array que llama al puntero de función ser: array [i] = (* getNextValue) (); ?
Nathan Fellman

40
El operador de desreferenciación es opcional con punteros de función, al igual que la dirección del operador. myfunc (...) = (* myfunc) (...) y & myfunc = myfunc
aib

1
@NathanFellman Acabo de leer la Programación Expert C y explica bien el puntero de la función.
Matt Clarkson el

1
@johnny Porque el estándar lo dice. Mira el comentario votado.
ab

3
@Patrick: populateArray está en una biblioteca (y se escribió hace 12 años) y usted escribió getNextRandomValue usted mismo (ayer); entonces no puede llamarlo directamente. Piense en una función de ordenación de biblioteca a la que le proporciona el comparador usted mismo.
aib

121

Aquí hay un ejemplo de devoluciones de llamada en C.

Supongamos que desea escribir un código que permita registrar las devoluciones de llamada cuando se produce algún evento.

Primero defina el tipo de función utilizada para la devolución de llamada:

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

Ahora, defina una función que se utiliza para registrar una devolución de llamada:

int event_cb_register(event_cb_t cb, void *userdata);

Así es como se vería el código que registra una devolución de llamada:

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);
...

En lo interno del despachador de eventos, la devolución de llamada puede almacenarse en una estructura que se parece a esto:

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

Así es como se ve el código que ejecuta una devolución de llamada.

struct event_cb *callback;

...

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

callback->cb(event, callback->data);

Justo lo que necesitaba. La parte de datos de usuario es muy útil si sus usuarios desean pasar datos personalizados (por ejemplo, identificadores de dispositivo) necesarios en la función de devolución de llamada.
uceumern

pregunta de verificación: ¿La devolución de llamada es typedef con un asterisco porque es un puntero a la dirección de la función? Si falta el asterisco, ¿sería incorrecto? Si eso es incorrecto, entonces hay dos estrellas que faltan en la biblioteca libsrtp de cisco en github: github.com/cisco/libsrtp/blob/… github.com/cisco/libsrtp/blob/…
twildeman

@twildeman Parece trivial responder su propia pregunta compilando en un modo Estándar C con advertencias. También puede escribir un programa de prueba minimizado. Código como los que se encuentran en libsrtpno da advertencias. Supongo, entonces, que cuando dicho tipo aparece como un argumento de función, se requiere 'decaer' a un puntero a función, al igual que las matrices decaen a los punteros a sus primeros elementos, por lo que sucede lo mismo al final de cualquier manera. Sin embargo, es interesante que las discusiones sobre los tipos de letra que he encontrado no echen un vistazo a este aspecto, sino que se centren en declarar prototipos o punteros con él
subrayado_d

No tengo idea de lo que hace, y no se puede compilar con éxito. ¿Alguien puede explicarlo de manera detallada o completar el resto del código para compilarlo correctamente?
Andy Lin

20

Un simple programa de devolución de llamada. Espero que responda tu pregunta.

#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;
}

9

Una función de devolución de llamada en C es el equivalente de un parámetro / variable de función asignado para ser utilizado dentro de otra función. Ejemplo de Wiki

En el siguiente código,

#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;
}

La función (* numberSource) dentro de la función llamar a PrintTwoNumbers es una función para "devolver la llamada" / ejecutar desde dentro de PrintTwoNumbers según lo dicte el código mientras se ejecuta.

Entonces, si tuviera algo así como una función pthread, podría asignar otra función para que se ejecute dentro del ciclo desde su creación de instancias.


6

Una devolución de llamada en C es una función que se proporciona a otra función para "volver a llamar" en algún momento cuando la otra función está haciendo su tarea.

Hay dos formas de utilizar una devolución de llamada : devolución de llamada síncrona y devolución de llamada asíncrona. Se proporciona una devolución de llamada síncrona a otra función que realizará alguna tarea y luego regresará a la persona que llama con la tarea completada. Se proporciona una devolución de llamada asíncrona a otra función que va a iniciar una tarea y luego volver a la persona que llama con la tarea posiblemente no completada.

Una devolución de llamada síncrona se usa generalmente para proporcionar un delegado a otra función a la que la otra función delega algún paso de la tarea. Ejemplos clásicos de esta delegación son las funciones bsearch()y qsort()la Biblioteca estándar de C. Ambas funciones toman una devolución de llamada que se utiliza durante la tarea que proporciona la función, de modo que el tipo de los datos que se buscan, en el caso de bsearch(), u ordenados, en el caso de qsort(), no necesitan ser conocidos por la función usado.

Por ejemplo, aquí hay un pequeño programa de muestra con el bsearch()uso de diferentes funciones de comparación, devoluciones de llamada síncronas. Al permitirnos delegar la comparación de datos a una función de devolución de llamada, la bsearch()función nos permite decidir en tiempo de ejecución qué tipo de comparación queremos usar. Esto es sincrónico porque cuando la bsearch()función regresa, la tarea está completa.

#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);
    }
}

Una devolución de llamada asincrónica es diferente en que cuando la función llamada a la que proporcionamos una devolución de llamada regresa, la tarea puede no completarse. Este tipo de devolución de llamada a menudo se usa con E / S asíncrona en la que se inicia una operación de E / S y luego, cuando se completa, se invoca la devolución de llamada.

En el siguiente programa, creamos un socket para escuchar las solicitudes de conexión TCP y cuando se recibe una solicitud, la función que realiza la escucha invoca la función de devolución de llamada proporcionada. Esta sencilla aplicación puede ejercerse ejecutándola en una ventana mientras usa la telnetutilidad o un navegador web para intentar conectarse en otra ventana.

Extraje la mayor parte del código WinSock del ejemplo que Microsoft proporciona con la accept()función en https://msdn.microsoft.com/en-us/library/windows/desktop/ms737526(v=vs.85).aspx

Esta aplicación inicia un listen()en el host local, 127.0.0.1, utilizando el puerto 8282 para que pueda utilizar telnet 127.0.0.1 8282o http://127.0.0.1:8282/.

Esta aplicación de muestra se creó como una aplicación de consola con Visual Studio 2017 Community Edition y está utilizando la versión de sockets Microsoft WinSock. Para una aplicación de Linux, las funciones de WinSock necesitarían ser reemplazadas por las alternativas de Linux y la biblioteca de hilos de Windows usaría pthreadsen su lugar.

#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);
}

Excelente respuesta, que muestra devoluciones de llamada sincrónicas y asincrónicas. Otro ejemplo concreto del uso de devoluciones de llamada asíncronas en C- * NIX son las señales asíncronas y sus manejadores de señales. Aquí hay una excelente descripción de cómo se procesan los manejadores de señales en Linux [enlace] ( stackoverflow.com/questions/6949025/… ).
Drlolly

4

Las devoluciones de llamada en C generalmente se implementan utilizando punteros de función y un puntero de datos asociado. Pasa sus on_event()punteros de función y datos a una función marco watch_events()(por ejemplo). Cuando ocurre un evento, su función se llama con sus datos y algunos datos específicos del evento.

Las devoluciones de llamada también se utilizan en la programación de la GUI. El tutorial GTK + tiene una buena sección sobre la teoría de señales y devoluciones de llamada .


2

Este artículo de Wikipedia tiene un ejemplo en C.

Un buen ejemplo es que los nuevos módulos escritos para aumentar el servidor web Apache se registran en el proceso principal de apache pasándoles punteros de función para que esas funciones se vuelvan a llamar para procesar las solicitudes de la página web.


0

Por lo general, esto se puede hacer usando un puntero de función, que es una variable especial que apunta a la ubicación de memoria de una función. Luego puede usar esto para llamar a la función con argumentos específicos. Por lo tanto, probablemente habrá una función que establezca la función de devolución de llamada. Esto aceptará un puntero de función y luego almacenará esa dirección en algún lugar donde pueda usarse. Después de eso, cuando se activa el evento especificado, llamará a esa función.


0

Es mucho más fácil entender una idea a través del ejemplo. Lo que se ha dicho sobre la función de devolución de llamada en C hasta ahora son excelentes respuestas, pero probablemente el mayor beneficio de usar la función es mantener el código limpio y ordenado.

Ejemplo

El siguiente código C implementa la ordenación rápida. La línea más interesante en el código a continuación es esta, donde podemos ver la función de devolución de llamada en acción:

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

Compare_s2b es el nombre de la función que qsort () está utilizando para llamar a la función. Esto mantiene qsort () tan ordenado (por lo tanto, más fácil de mantener). Simplemente llama a una función por su nombre desde dentro de otra función (por supuesto, la declaración del prototipo de la función, al menos, debe excluirse antes de que pueda llamarse desde otra función).

El 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);
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.