Co to jest „callback” w C i jak są one realizowane?

153

Z czytania, którego dokonałem, Core Audio w dużym stopniu opiera się na callbackach (i C ++, ale to już inna historia).

Rozumiem koncepcję (rodzaj) ustawiania funkcji, która jest wielokrotnie wywoływana przez inną funkcję w celu wykonania zadania. Po prostu nie rozumiem, jak się konfigurują i jak faktycznie działają. Wszelkie przykłady będą mile widziane.

noizetoys
źródło

Odpowiedzi:

203

W C nie ma „wywołania zwrotnego” - nie więcej niż jakakolwiek inna generyczna koncepcja programowania.

Są zaimplementowane za pomocą wskaźników funkcji. Oto przykład:

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

W tym przypadku populate_arrayfunkcja przyjmuje wskaźnik funkcji jako trzeci parametr i wywołuje go w celu pobrania wartości do zapełnienia tablicy. Napisaliśmy wywołanie zwrotne getNextRandomValue, które zwraca losową wartość i przekazaliśmy do niej wskaźnik populate_array. populate_arraywywoła naszą funkcję zwrotną 10 razy i przypisze zwrócone wartości do elementów w podanej tablicy.

aib
źródło
2
Mogę się mylić, ale linia w populate_array, która wywołuje wskaźnik funkcji, nie powinna być następująca: array [i] = (* getNextValue) (); ?
Nathan Fellman
40
Operator wyłuskiwania jest opcjonalny w przypadku wskaźników do funkcji, podobnie jak operator addressof. myfunc (...) = (* myfunc) (...) and & myfunc = myfunc
aib
1
@NathanFellman Właśnie przeczytałem Expert C Programming i dobrze wyjaśnia on wywołanie wskaźnika funkcji.
Matt Clarkson
1
@johnny Ponieważ tak mówi norma. Spójrz na pozytywnie rozpatrzony komentarz.
aib
3
@Patrick: populateArray jest w bibliotece (i został napisany 12 lat temu) i sam napisałeś getNextRandomValue (wczoraj); więc nie może nazywać tego bezpośrednio. Pomyśl o funkcji sortowania w bibliotece, do której sam dostarczasz komparator.
aib
121

Oto przykład wywołań zwrotnych w C.

Powiedzmy, że chcesz napisać kod, który umożliwi rejestrowanie wywołań zwrotnych w przypadku wystąpienia jakiegoś zdarzenia.

Najpierw zdefiniuj typ funkcji używanej do wywołania zwrotnego:

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

Teraz zdefiniuj funkcję, która zostanie użyta do zarejestrowania wywołania zwrotnego:

int event_cb_register(event_cb_t cb, void *userdata);

Tak wyglądałby kod rejestrujący wywołanie zwrotne:

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

We wnętrzu modułu wywołującego zdarzenia callback może być przechowywany w strukturze, która wygląda mniej więcej tak:

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

Tak wygląda kod, który wykonuje wywołanie zwrotne.

struct event_cb *callback;

...

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

callback->cb(event, callback->data);
Russell Bryant
źródło
Dokładnie to, czego potrzebowałem. Część z danymi użytkownika jest bardzo pomocna, jeśli użytkownicy chcą przekazywać niestandardowe dane (np. Uchwyty urządzeń) wymagane w funkcji wywołania zwrotnego.
uceumern
pytanie weryfikacyjne: czy typedef wywołania zwrotnego jest oznaczony gwiazdką, ponieważ jest wskaźnikiem do adresu funkcji? Jeśli brakuje gwiazdki, czy byłoby to niepoprawne? Jeśli to nieprawda, to w bibliotece libsrtp firmy Cisco na githubie brakuje dwóch gwiazdek: github.com/cisco/libsrtp/blob/ ... github.com/cisco/libsrtp/blob/…
twildeman
@twildeman Wydaje się trywialne, aby odpowiedzieć na własne pytanie przez kompilację w standardowym trybie C z włączonymi ostrzeżeniami. Możesz także napisać zminimalizowany program testowy. Kod taki jak ten libsrtpnie daje żadnych ostrzeżeń. Zakładam więc, że kiedy taki typ pojawia się jako argument funkcji, to wymaga się `` rozpadu '' na wskaźnik-do-funkcji, tak jak tablice rozpadają się na wskaźniki do ich pierwszych elementów, więc to samo dzieje się na końcu tak czy inaczej. To jest ciekawe, jednak, że dyskusje takich typedefs znalazłem nawet nie spojrzał na ten aspekt, a skupić się na deklarując prototypy lub wskaźniki z nim
underscore_d
Nie mam pojęcia, co to robi, i nie można go pomyślnie skompilować. Czy ktoś może to szczegółowo wyjaśnić lub wypełnić resztę kodu, aby pomyślnie się skompilować?
Andy Lin
20

Prosty program oddzwaniania. Mam nadzieję, że to odpowiada na twoje pytanie.

#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
źródło
9

Funkcja zwrotna w C jest odpowiednikiem parametru / zmiennej funkcji przypisanej do użycia w innej funkcji. Przykład Wiki

W poniższym kodzie

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

Funkcja (* numberSource) wewnątrz wywołania funkcji PrintTwoNumbers jest funkcją "oddzwaniania" / wykonywania z wnętrza PrintTwoNumbers, zgodnie z kodem w trakcie jego działania.

Więc jeśli masz coś w rodzaju funkcji pthread, możesz przypisać inną funkcję do uruchamiania wewnątrz pętli z jej wystąpienia.

daemondave
źródło
6

Funkcja zwrotna w C to funkcja udostępniana innej funkcji w celu „oddzwonienia” w pewnym momencie, gdy inna funkcja wykonuje swoje zadanie.

Istnieją dwa sposoby wykorzystania wywołania zwrotnego : synchroniczny i asynchroniczny callback zwrotna. Synchroniczne wywołanie zwrotne jest dostarczane do innej funkcji, która wykona jakieś zadanie, a następnie wróci do dzwoniącego po zakończeniu zadania. Asynchroniczne wywołanie zwrotne jest dostarczane do innej funkcji, która ma rozpocząć zadanie, a następnie wrócić do obiektu wywołującego z zadaniem prawdopodobnie nie zakończonym.

Synchroniczne wywołanie zwrotne jest zwykle używane w celu zapewnienia delegata innej funkcji, do której druga funkcja deleguje pewien krok zadania. Klasycznymi przykładami tej delegacji są funkcje bsearch()i qsort()biblioteka standardowa C. Obie te funkcje przyjmują wywołanie zwrotne, które jest używane podczas zadania, które funkcja zapewnia, tak że typ wyszukiwanych danych, w przypadku bsearch()lub sortowania, w przypadku qsort(), nie musi być znany funkcji będącej używany.

Na przykład tutaj jest mały przykładowy program bsearch()wykorzystujący różne funkcje porównawcze, synchroniczne wywołania zwrotne. Umożliwiając nam delegowanie porównania danych do funkcji zwrotnej, bsearch()funkcja ta pozwala nam zdecydować w czasie wykonywania, jakiego rodzaju porównania chcemy użyć. Jest to synchroniczne, ponieważ gdy bsearch()funkcja zwraca, zadanie jest zakończone.

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

Asynchroniczne wywołanie zwrotne różni się tym, że gdy wywoływana funkcja, której dostarczamy, powraca, zadanie może nie zostać ukończone. Ten typ wywołania zwrotnego jest często używany w przypadku asynchronicznych operacji we / wy, w których operacja we / wy jest uruchamiana, a po jej zakończeniu wywoływana jest funkcja zwrotna.

W poniższym programie tworzymy gniazdo do nasłuchiwania żądań połączenia TCP, a po odebraniu żądania funkcja nasłuchująca wywołuje dostarczoną funkcję zwrotną. Tę prostą aplikację można uruchomić, uruchamiając ją w jednym oknie, podczas gdy telnetnarzędzie lub przeglądarka internetowa próbuje połączyć się w innym oknie.

Podniosłem większość kodu WinSock z przykładu dostarczonego przez Microsoft z accept()funkcją pod adresem https://msdn.microsoft.com/en-us/library/windows/desktop/ms737526(v=vs.85).aspx

Aplikacja ta rozpoczyna się listen()na lokalnym komputerze, 127.0.0.1, za pomocą portu 8282, więc można użyć jednej telnet 127.0.0.1 8282lub http://127.0.0.1:8282/.

Ta przykładowa aplikacja została utworzona jako aplikacja konsolowa w programie Visual Studio 2017 Community Edition i korzysta z wersji gniazd Microsoft WinSock. W przypadku aplikacji dla systemu Linux funkcje WinSock musiałyby zostać zastąpione alternatywami dla systemu Linux, a biblioteka wątków systemu Windows byłaby używana pthreadszamiast tego.

#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
źródło
Doskonała odpowiedź, pokazująca zarówno synchroniczne, jak i asynchroniczne wywołania zwrotne. Innym konkretnym przykładem użycia asynchronicznych wywołań zwrotnych w C- * NIX są sygnały asynchroniczne i ich programy obsługi sygnałów. Oto doskonały opis przetwarzania programów obsługi sygnałów w Linuksie [link] ( stackoverflow.com/questions/6949025/ ... ).
drlolly
4

Wywołania zwrotne w C są zwykle implementowane przy użyciu wskaźników funkcji i powiązanego wskaźnika danych. Przekazujesz swoją funkcję on_event()i wskaźniki danych do funkcji struktury watch_events()(na przykład). Gdy ma miejsce zdarzenie, wywoływana jest funkcja z Twoimi danymi i niektórymi danymi specyficznymi dla zdarzenia.

Callbacki są również używane w programowaniu GUI. GTK + poradnik ma sekcję Nicei na teorii sygnałów i wywołania zwrotne .

John Millikin
źródło
2

Ten artykuł na Wikipedii ma przykład w C.

Dobrym przykładem jest to, że nowe moduły napisane w celu rozszerzenia serwera WWW Apache rejestrują się o główny proces Apache, przekazując im wskaźniki funkcji, aby te funkcje były wywoływane z powrotem w celu przetwarzania żądań stron internetowych.

Leonard
źródło
0

Zwykle można to zrobić za pomocą wskaźnika funkcji, czyli specjalnej zmiennej, która wskazuje lokalizację funkcji w pamięci. Możesz następnie użyć tego do wywołania funkcji z określonymi argumentami. Więc prawdopodobnie będzie funkcja, która ustawia funkcję zwrotną. To zaakceptuje wskaźnik funkcji, a następnie zapisze ten adres gdzieś, gdzie może być używany. Następnie, gdy określone zdarzenie zostanie wyzwolone, wywoła tę funkcję.

mdec
źródło
0

O wiele łatwiej jest zrozumieć pomysł na przykładzie. To, co dotychczas powiedziano o funkcji wywołania zwrotnego w C, to świetne odpowiedzi, ale prawdopodobnie największą zaletą korzystania z tej funkcji jest utrzymanie kodu w czystości i porządku.

Przykład

Poniższy kod C implementuje szybkie sortowanie. Najciekawszą linią w poniższym kodzie jest ta, w której możemy zobaczyć funkcję callback w akcji:

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

Porównanie_s2b to nazwa funkcji, której qsort () używa do wywołania funkcji. Dzięki temu qsort () jest uporządkowana (stąd łatwiejsza w utrzymaniu). Po prostu wywołujesz funkcję po nazwie z wnętrza innej funkcji (oczywiście przynajmniej deklaracja prototypu funkcji musi być wcześniejsza, zanim będzie można ją wywołać z innej funkcji).

Kompletny kod

#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);
}
Jak gdyby
źródło