Qu'est-ce qu'un «callback» en C et comment sont-ils implémentés?


153

D'après la lecture que j'ai faite, Core Audio s'appuie fortement sur les rappels (et C ++, mais c'est une autre histoire).

Je comprends le concept (en quelque sorte) de la configuration d'une fonction qui est appelée à plusieurs reprises par une autre fonction pour accomplir une tâche. Je ne comprends tout simplement pas comment ils sont mis en place et comment ils fonctionnent réellement. Tous les exemples seraient appréciés.

Réponses:


203

Il n'y a pas de "rappel" en C - pas plus que tout autre concept de programmation générique.

Ils sont implémentés à l'aide de pointeurs de fonction. Voici un exemple:

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

Ici, la populate_arrayfonction prend un pointeur de fonction comme troisième paramètre et l'appelle pour obtenir les valeurs avec lesquelles remplir le tableau. Nous avons écrit le rappel getNextRandomValue, qui retourne une valeur aléatoire, et lui avons passé un pointeur vers populate_array. populate_arrayappellera notre fonction de rappel 10 fois et attribuera les valeurs renvoyées aux éléments du tableau donné.


2
Je me trompe peut-être ici, mais la ligne de populate_array qui appelle le pointeur de fonction ne devrait-elle pas être: array [i] = (* getNextValue) (); ?
Nathan Fellman

40
L'opérateur de déréférencement est facultatif avec les pointeurs de fonction, tout comme l'opérateur addressof. myfunc (...) = (* myfunc) (...) and & myfunc = myfunc
aib

1
@NathanFellman Je viens de lire la programmation Expert C et cela explique bien l'appel du pointeur de fonction.
Matt Clarkson

1
@johnny Parce que la norme le dit. Regardez le commentaire voté.
aib

3
@Patrick: populateArray est dans une bibliothèque (et a été écrit il y a 12 ans) et vous avez écrit vous-même getNextRandomValue (hier); il ne peut donc pas l'appeler directement. Pensez à une fonction de tri de bibliothèque à laquelle vous fournissez vous-même le comparateur.
aib

121

Voici un exemple de callbacks en C.

Supposons que vous souhaitiez écrire du code permettant d'appeler les rappels d'enregistrement lorsqu'un événement se produit.

Définissez d'abord le type de fonction utilisée pour le rappel:

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

Maintenant, définissez une fonction qui est utilisée pour enregistrer un rappel:

int event_cb_register(event_cb_t cb, void *userdata);

Voici à quoi ressemblerait le code qui enregistre un rappel:

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

Dans les composants internes du répartiteur d'événements, le rappel peut être stocké dans une structure qui ressemble à ceci:

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

Voici à quoi ressemble le code qui exécute un rappel.

struct event_cb *callback;

...

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

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

Juste ce dont j'avais besoin. La partie userdata est très utile si vos utilisateurs veulent transmettre des données personnalisées (par exemple des poignées de périphérique) requises dans la fonction de rappel.
uceumern

question de vérification: le rappel est-il typedef avec un astérisque car il s'agit d'un pointeur vers l'adresse de la fonction? Si l'astérisque est manquant, est-ce que ce serait incorrect? Si cela est incorrect, alors il y a deux étoiles manquantes dans la bibliothèque libsrtp de Cisco sur github: github.com/cisco/libsrtp/blob /
twildeman

@twildeman Il semble trivial de répondre à votre propre question en compilant en mode C Standard avec des avertissements activés. Vous pouvez également écrire un programme de test réduit. Un code tel que celui de libsrtpne donne aucun avertissement. Je présume, alors, que lorsqu'un tel type apparaît comme un argument de fonction, il est nécessaire de se `` désintégrer '' en un pointeur vers une fonction, tout comme les tableaux se désintègrent en pointeurs vers leurs premiers éléments, donc la même chose se produit à la fin d'une manière ou d'une autre. Il est intéressant, cependant, que les discussions sur ces typedefs que j'ai trouvées ne jettent même pas un coup d'œil sur cet aspect, mais se concentrent plutôt sur la déclaration de prototypes ou de pointeurs avec
underscore_d

Je n'ai aucune idée de ce que cela fait, et il ne peut pas être compilé avec succès. Quelqu'un peut-il l'expliquer de manière détaillée ou remplir le reste du code pour une compilation réussie?
Andy Lin

20

Un programme de rappel simple. J'espère que cela répond à votre question.

#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

Une fonction de rappel en C est l'équivalent d'un paramètre / variable de fonction assigné pour être utilisé dans une autre fonction.Exemple de wiki

Dans le code ci-dessous,

#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 fonction (* numberSource) à l'intérieur de l'appel de fonction PrintTwoNumbers est une fonction à «rappeler» / exécuter à partir de PrintTwoNumbers comme dicté par le code pendant qu'il s'exécute.

Donc, si vous aviez quelque chose comme une fonction pthread, vous pourriez assigner une autre fonction à exécuter à l'intérieur de la boucle à partir de son instanciation.


6

Un rappel en C est une fonction qui est fournie à une autre fonction pour "rappeler" à un moment donné lorsque l'autre fonction exécute sa tâche.

Il y a deux façons d'utiliser un rappel : le rappel synchrone et le rappel asynchrone. Un rappel synchrone est fourni à une autre fonction qui va effectuer une tâche puis revenir à l'appelant avec la tâche terminée. Un rappel asynchrone est fourni à une autre fonction qui va démarrer une tâche puis revenir à l'appelant avec la tâche éventuellement non terminée.

Un rappel synchrone est généralement utilisé pour fournir un délégué à une autre fonction à laquelle l'autre fonction délègue une étape de la tâche. Des exemples classiques de cette délégation sont les fonctions bsearch()et qsort()de la bibliothèque standard C. Ces deux fonctions prennent un rappel qui est utilisé pendant la tâche que la fonction fournit afin que le type des données recherchées, dans le cas de bsearch(), ou triées, dans le cas de qsort(), n'ait pas besoin d'être connu par la fonction étant utilisé.

Par exemple, voici un petit exemple de programme bsearch()utilisant différentes fonctions de comparaison, des rappels synchrones. En nous permettant de déléguer la comparaison des données à une fonction de rappel, la bsearch()fonction nous permet de décider au moment de l'exécution quel type de comparaison nous voulons utiliser. Ceci est synchrone car lorsque la bsearch()fonction retourne, la tâche est terminée.

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

Un rappel asynchrone est différent en ce que lorsque la fonction appelée à laquelle nous fournissons un rappel revient, la tâche peut ne pas être terminée. Ce type de rappel est souvent utilisé avec les E / S asynchrones dans lesquelles une opération d'E / S est lancée, puis, une fois terminée, le rappel est appelé.

Dans le programme suivant, nous créons une socket pour écouter les demandes de connexion TCP et lorsqu'une demande est reçue, la fonction qui fait l'écoute appelle alors la fonction de rappel fournie. Cette application simple peut être exercée en l'exécutant dans une fenêtre tout en utilisant l' telnetutilitaire ou un navigateur Web pour tenter de se connecter dans une autre fenêtre.

J'ai extrait la plupart du code WinSock de l'exemple fourni par Microsoft avec la accept()fonction à l' adresse https://msdn.microsoft.com/en-us/library/windows/desktop/ms737526(v=vs.85).aspx

Cette application démarre un listen()sur l'hôte local, 127.0.0.1, en utilisant le port 8282 afin que vous puissiez utiliser soit telnet 127.0.0.1 8282ou http://127.0.0.1:8282/.

Cet exemple d'application a été créé en tant qu'application console avec Visual Studio 2017 Community Edition et utilise la version Microsoft WinSock des sockets. Pour une application Linux, les fonctions WinSock devraient être remplacées par les alternatives Linux et la bibliothèque de threads Windows utiliserait à la pthreadsplace.

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

Excellente réponse, montrant les rappels synchrones et asynchrones. Un autre exemple concret de l'utilisation des rappels asynchrones dans C- * NIX sont les signaux asynchrones et leurs gestionnaires de signaux. Voici une excellente description de la façon dont les gestionnaires de signaux sont traités sous Linux [lien] ( stackoverflow.com/questions/6949025/… ).
drlolly

4

Les rappels en C sont généralement implémentés à l'aide de pointeurs de fonction et d'un pointeur de données associé. Vous transmettez votre fonction on_event()et vos pointeurs de données à une fonction de cadre watch_events()(par exemple). Lorsqu'un événement se produit, votre fonction est appelée avec vos données et certaines données spécifiques à l'événement.

Les rappels sont également utilisés dans la programmation GUI. Le tutoriel GTK + a une belle section sur la théorie des signaux et des rappels .


2

Cet article de wikipedia a un exemple en C.

Un bon exemple est que les nouveaux modules écrits pour augmenter le registre du serveur Web Apache avec le processus apache principal en leur passant des pointeurs de fonction afin que ces fonctions soient rappelées pour traiter les requêtes de page Web.


0

Habituellement, cela peut être fait en utilisant un pointeur de fonction, qui est une variable spéciale qui pointe vers l'emplacement mémoire d'une fonction. Vous pouvez ensuite l'utiliser pour appeler la fonction avec des arguments spécifiques. Il y aura donc probablement une fonction qui définit la fonction de rappel. Cela acceptera un pointeur de fonction, puis stockera cette adresse quelque part où elle pourra être utilisée. Après cela, lorsque l'événement spécifié est déclenché, il appellera cette fonction.


0

Il est beaucoup plus facile de comprendre une idée par l'exemple. Ce qui a été dit sur la fonction de rappel en C jusqu'à présent sont d'excellentes réponses, mais probablement le plus grand avantage de l'utilisation de cette fonctionnalité est de garder le code propre et épuré.

Exemple

Le code C suivant implémente le tri rapide. La ligne la plus intéressante dans le code ci-dessous est celle-ci, où nous pouvons voir la fonction de rappel en action:

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

Compare_s2b est le nom de la fonction que qsort () utilise pour appeler la fonction. Cela garde qsort () si épuré (donc plus facile à maintenir). Vous appelez simplement une fonction par son nom depuis une autre fonction (bien sûr, la déclaration du prototype de fonction doit au moins précéder avant de pouvoir être appelée depuis une autre fonction).

Le code complet

#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);
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.