A C függvénypointere azt teszi lehetővé, hogy egy algoritmus vagy eseményforrás működésének egy részét a hívó határozza meg. Rendezésnél ez az összehasonlítás, bejárásnál az elemen végzett művelet, eseménykezelésnél az esemény feldolgozása. A visszahívás (callback) azonban rendszerint állapotot is igényel: számlálót, konfigurációt vagy egy feldolgozó objektumot.
C-ben ezt jellemzően két összetartozó érték képviseli: egy
függvénypointer és egy void * context pointer. A helyes
interfész nemcsak ezek típusát, hanem élettartamukat, tulajdonlásukat és
hibaszerződésüket is meghatározza.
A deklarátorokról szóló rész alapján már
olvasható a függvénypointer deklarációja. A void * tárgyalásából pedig azt kell
felidézni, hogy az általános objektumpointer nem őrzi meg a referált
objektum típusinformációját.
ISO C17: a függvénypointer függvényre mutató érték. Nem objektum tárolóját jelöli, és nem alkalmazható rá pointeraritmetika.
Az alábbi deklarációrészlet egy összehasonlító függvény pointertípusát nevezi el:
/* ISO C17; deklarációrészlet. */
typedef int (*compare_fn)(const void *, const void *);A compare_fn olyan pointertípus, amelyen keresztül két
const void * argumentumot fogadó, int értéket
visszaadó függvény hívható. A függvény neve pointert igénylő
kifejezésben rendszerint automatikusan függvénypointerré alakul; az
explicit & is használható. Ha fn megfelelő
függvénypointer, akkor az fn(a, b) és a
(*fn)(a, b) ugyanazt a hívást fejezi ki.
A hívás feltételei:
A kompatibilitás nem pusztán az argumentumok számát jelenti. A visszatérési típus, a paramétertípusok és a variadikus jelleg is számít. Célszerű a callbacket eleve az API-ban megadott prototípussal definiálni.
ISO C17: különböző függvénypointer-típusok között lehet explicit konverziót végezni, és az eredeti típusra visszakonvertált pointer egyenlő az eredetivel. Ez azonban nem teszi megengedetté az inkompatibilis típuson keresztüli hívást: annak viselkedése nem definiált. Egy cast nem alakítja át a függvény paraméterkezelését.
Paraméter nélküli C17 függvénynél a (void) prototípust
használd. Az üres () deklaráció C17-ben nem ugyanezt
mondja: nem ad paramétertípus-információt.
A context pointer (context pointer, felhasználói állapotpointer) az algoritmus számára átlátszatlan adat. Az algoritmus tárolhatja vagy továbbadhatja, de csak az a callback értelmezheti, amely ismeri a mögötte álló típust.
A pointerpár fogalmilag egyetlen egység:
Ha a függvény az egyik regisztrációból, az állapot egy másikból származik, a típusellenőrzés ezt többnyire nem észleli. A callback hibás objektumtípust feltételezhet, vagy érvénytelen tárolóhoz férhet hozzá.
Két élettartam-modellt kell megkülönböztetni.
Szinkron visszahívásnál a feldolgozó függvény nem őrzi meg a pointerpárt. Minden callback befejeződik, mielőtt a feldolgozó visszatér. A hívó megfelelően élő lokális objektuma ezért használható contextként.
Tárolt regisztrációnál az eseményforrás megőrzi a pointerpárt. A contextnek minden lehetséges későbbi hívás alatt élnie kell. Nem elég addig megőrizni, amíg a regisztrációs függvény visszatér.
Az alábbi példában minden hozzáférés kölcsönzött:
| Erőforrás | Tulajdonos, illetve kezelő | Elvárt élettartam | Érvénytelenítő esemény |
|---|---|---|---|
| Bejárt tömb tárolója | A main lokális tömbobjektuma |
A teljes bejárás alatt | A tömb élettartamának vége |
| Bejárási context | A main lokális állapotobjektuma |
A walk_ints visszatéréséig |
Az állapot élettartamának vége |
| Regisztrált context | A main lokális állapotobjektuma |
Minden hívás befejezéséig és a leiratkozásig | Korai élettartamvég |
| Eseményregisztráció | A hívó által kezelt event_slot |
A regisztráció használata alatt | A slot élettartamának vége |
| Callback függvény | A program kódja | Minden lehetséges hívás alatt | Platformfüggő modulkezelésnél például túl korai modulkitöltés helyett modul-kirakás |
Az utolsó eset nem az ISO C dinamikusbetöltési szolgáltatása: ha megosztott modulok kirakása is lehetséges, annak külön platformfüggő szerződés kell. Az alábbi önálló program függvényei a program futása alatt rendelkezésre állnak.
A következő interfész három eredményt különböztet meg:
CB_OK: a feldolgozás folytatható;CB_STOP: szándékos korai megállás;CB_ERROR: feldolgozási vagy szerződésellenőrzéssel
felismert hiba.A látogató (visitor) értékként kapja az aktuális egész számot. Nem kap az elemre mutató, eltárolható pointert. A bejáró nem tartja meg sem a callbacket, sem a contextet.
Az eseménypélda egyetlen int eseményértéket továbbít. A
slot már megőrzi a pointerpárt, de nem veszi át a context
tulajdonlását.
Példaprofil: ISO C17, hosted Linux, x86-64 System V ABI, LP64. Választott reprodukciós eszközprofil: GCC 14.2.0 vagy Clang 19.1.7, glibc 2.40. A kód nem igényel GNU nyelvi bővítést. Az alábbiakhoz nem tartozik itt tényleges fordítási vagy futási eredmény.
/* callbacks.c */
#include <limits.h>
#include <stddef.h>
#include <stdlib.h>
enum cb_result {
CB_OK,
CB_STOP,
CB_ERROR
};
typedef enum cb_result (*callback_fn)(int value, void *ctx);
struct counter {
int threshold;
size_t limit;
size_t accepted;
};
static enum cb_result accept_limited(int value, void *ctx)
{
if (ctx == NULL)
return CB_ERROR;
struct counter *state = ctx;
if (value < state->threshold)
return CB_OK;
if (state->accepted >= state->limit)
return CB_STOP;
++state->accepted;
return CB_OK;
}
static enum cb_result walk_ints(
const int *items, size_t count, callback_fn fn, void *ctx)
{
if (fn == NULL || (count != 0 && items == NULL))
return CB_ERROR;
for (size_t i = 0; i < count; ++i) {
enum cb_result result = fn(items[i], ctx);
if (result == CB_STOP)
return CB_STOP;
if (result != CB_OK)
return CB_ERROR;
}
return CB_OK;
}
static int compare_ints(const void *left, const void *right)
{
const int *a = left;
const int *b = right;
return (*a > *b) - (*a < *b);
}
struct event_slot {
callback_fn fn;
void *ctx;
int active;
};
static enum cb_result event_slot_set(
struct event_slot *slot, callback_fn fn, void *ctx)
{
if (slot == NULL || slot->active)
return CB_ERROR;
slot->fn = fn;
slot->ctx = fn != NULL ? ctx : NULL;
return CB_OK;
}
static enum cb_result event_slot_emit(struct event_slot *slot, int value)
{
if (slot == NULL || slot->active || slot->fn == NULL)
return CB_ERROR;
slot->active = 1;
enum cb_result result = slot->fn(value, slot->ctx);
slot->active = 0;
if (result != CB_OK && result != CB_STOP)
return CB_ERROR;
return result;
}
int main(void)
{
int values[] = { INT_MAX, -3, 0, INT_MIN, 4 };
size_t count = sizeof values / sizeof values[0];
qsort(values, count, sizeof values[0], compare_ints);
struct counter visitor_state = { 0, 2, 0 };
enum cb_result result =
walk_ints(values, count, accept_limited, &visitor_state);
if (result != CB_STOP || visitor_state.accepted != 2)
return EXIT_FAILURE;
struct counter event_state = { 10, 1, 0 };
struct event_slot slot = { 0 };
if (event_slot_set(&slot, accept_limited, &event_state) != CB_OK)
return EXIT_FAILURE;
result = event_slot_emit(&slot, 12);
if (event_slot_set(&slot, NULL, NULL) != CB_OK)
return EXIT_FAILURE;
if (result != CB_OK || event_state.accepted != 1)
return EXIT_FAILURE;
return EXIT_SUCCESS;
}A kiválasztott profilra beállított fordítókkal a külön tanulói labor buildparancsai:
gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -Wstrict-prototypes \
-O0 -g callbacks.c -o callbacks-gcc
clang -std=c17 -Wall -Wextra -Wpedantic -Wconversion -Wstrict-prototypes \
-O0 -g callbacks.c -o callbacks-clangA rendezésből következő várt sorrend
INT_MIN, -3, 0, 4, INT_MAX. A visitor az első két értéket
átugorja, majd a 0 és 4 elfogadásával eléri a
kettes korlátot. Az INT_MAX feldolgozásakor megállást
kér.
Fontos különbség: a korlát elérése még nem állítja
le azonnal ezt a callbacket. A következő elfogadható érték váltja ki a
CB_STOP eredményt. Emiatt pontosan két elfogadható elemet
tartalmazó bemenetnél a bejárás CB_OK eredménnyel is
befejeződhet.
A számláló nem csordul túl: növelés csak akkor történik, ha
Így növelés után
,
amely eleve ábrázolható size_t érték.
A nullvizsgálatok nem általános pointerérvényesség-vizsgálatok. A hívó további kötelezettségei:
items legalább count olvasható
int elemet jelöl;accept_limited contextje élő, megfelelően igazított
struct counter;A hibás típusú vagy dangling contextből nem garantáltan lesz
CB_ERROR: az ilyen hozzáférés UB lehet. A callback korábbi
mellékhatásait sem CB_STOP, sem CB_ERROR nem
vonja vissza. Ez az API nem tranzakció.
qsort
comparatorának követelményeiISO C17 könyvtári szerződés: a qsort
comparatorának negatív, nulla vagy pozitív eredménye rendre kisebb,
egyenértékű vagy nagyobb elemet jelent. Nem szükséges pontosan
-1, 0, 1 értékeket
visszaadni.
Az alábbi gyakori megoldás hibás:
/* Hibás C17 kódrészlet: signed túlcsordulás miatt UB lehet. */
/* return *a - *b; */Például az INT_MAX - (-3) matematikai eredménye nem
ábrázolható int típussal.
A helyes változat csak relációs eredményeket von ki:
Bizonyításvázlat: a relációs operátorok
int típusú 0 vagy 1 eredményt
adnak. Ha
,
az eredmény
;
egyenlőségnél
;
ha
,
akkor
.
Mindhárom ábrázolható, az eredeti számok kivonása elmarad.
A comparatornak következetes rendezési viszonyt kell megadnia. Ne
módosítsa az összehasonlított elemeket, és ne változtassa a sorrendet
számláló, véletlen vagy idő alapján. A rendező mozgatja az elemeket,
ezért az aktuális tárolási cím nem megfelelő kulcs egy logikai rekord
azonosítására. Egyenértékű elemek eredeti sorrendjének megőrzését a
qsort nem garantálja.
A szabványos C17 qsort nem kap contextet, és nincs külön
comparator-hibacsatornája. A nulla nem hiba, hanem egyenértékűség.
Contextes platformváltozatok léteznek, de nevüket, paramétersorrendjüket
és támogatásukat külön kell ellenőrizni.
A reentrancia (reentrancy) azt jelenti, hogy egy művelet újra meghívható, miközben egy korábbi végrehajtása még nem fejeződött be. Ez egyszálú programban is előfordulhat: a callback közvetve újabb eseménykibocsátást indít.
A walk_ints nem használ közös globális állapotot.
Független adatokkal és contexttel egymásba ágyazható. Ugyanannak a
módosuló contextnek az újrahasználatához viszont külön alkalmazásszintű
szabály szükséges. A globális változóval pótolt
qsort-context ezért törékeny: egy belső rendezés
felülírhatja a külső konfigurációját.
Az eseményslot más döntést hoz: az active jelzővel
megtiltja ugyanazon slot visszatérő kibocsátását és callback alatti
átregisztrálását. Ilyenkor CB_ERROR keletkezik.
Ez csak az API-n keresztüli módosításokat védi. A callback nem
írhatja közvetlenül át a slot mezőit, nem szüntetheti meg a slot vagy
context élettartamát, és nem kerülheti meg a normál visszatérést. Az
active közönséges int, tehát ez
egyszálú szerződés, nem szálbiztonsági mechanizmus.
Általános eseménykezelőben a leiratkozás önmagában nem feltétlenül jelenti, hogy nincs már folyamatban hívás. Contextet csak akkor szabad megszüntetni, amikor új hívás már nem indulhat, és minden korábbi befejeződött. A példabeli szinkron, egyszálú modellben az inaktív állapotban sikeres leiratkozás ezt biztosítja.
ISO C17 hordozhatósági határ: az objektumpointer
void *-rá, majd eredeti objektumpointer-típusára történő
konverziójának garanciája nem általánosítható függvénypointerre.
Ezért a függvénycímet ne rejtsd a contextbe. A példában külön
callback_fn és void * mező szerepel. Egyenlő
gépi méretük egy adott ABI-n nem bizonyítja a konverzió hordozhatóságát.
Ha egy platform API-ja külön garanciát ad, az platformfüggő szerződés,
nem általános ISO C szabály.
| Hibaminta | Következmény | Javítás |
|---|---|---|
| Inkompatibilis callback castolása | A hívás UB lehet | Megfelelő prototípusú adapterfüggvény |
| Lokális context megőrzése a visszatérés után | Dangling hozzáférés | Hosszabb élettartam vagy időben történő leiratkozás |
| Nem null pointer automatikusan érvényesnek tekintése | Típus- vagy élettartamhiba | Explicit hívói előfeltételek |
| Context felszabadítása callback közben | Aktív vagy későbbi hozzáférés sérülhet | Dokumentált leállítási és felszabadítási sorrend |
| Comparatorban kivonás | Signed túlcsordulás | Relációs eredmények különbsége |
| Hibajelzés után teljes visszagörgetés feltételezése | Részlegesen módosított állapot | Mellékhatások és rollback külön szerződése |
Egy függvény lokális struct counter objektum címét
regisztrálja egy nála hosszabb életű eseményslotba, majd visszatér.
Miért hibás ez akkor is, ha a regisztráció sikeres?
Írj a példához illeszkedő callbacket, amely csak számlálja a negatív
értékeket. Ha a számláló már SIZE_MAX, adjon
CB_ERROR eredményt változtatás nélkül.
A slot callbackje ugyanazon slotra ismét meghívja az
event_slot_emit függvényt. Mi történik? Mi változna, ha a
külső callback longjmp segítségével hagyná el a hívási
láncot?
A regisztráció pointerértéket másol, nem contextobjektumot. A lokális objektum élettartama a függvényből visszatérve véget ér; a későbbi callback nem férhet hozzá.
Javításként a context lehet a hívó hosszabb életű objektuma, vagy a regisztrációt még az élettartam vége előtt meg kell szüntetni. Az ellenőrzési feltétel: minden lehetséges callback-hívás teljes egészében a context élettartamába essen.
A következő ISO C17 részlet a példabeli típusdefiníciók után illeszthető be:
#include <stdint.h>
struct negative_count {
size_t value;
};
static enum cb_result count_negative(int value, void *ctx)
{
if (ctx == NULL)
return CB_ERROR;
struct negative_count *state = ctx;
if (value >= 0)
return CB_OK;
if (state->value == SIZE_MAX)
return CB_ERROR;
++state->value;
return CB_OK;
}A hívó tulajdonában álló contextnek élő
struct negative_count objektumot kell jelölnie. Ellenőrzési
esetek: nemnegatív bemenetnél nincs változás; negatív bemenetnél
SIZE_MAX - 1 értékről még megengedett a növelés;
SIZE_MAX esetén hiba történik, módosítás nélkül.
A belső kibocsátás active != 0 miatt
CB_ERROR eredménnyel visszatér, újabb callback nélkül. Ha a
külső callback normálisan visszatér, az eredeti kibocsátás nullázza az
active mezőt.
A longjmp átugorhatná ezt a nullázást, ezért sérti a
callback normál visszatérését előíró szerződést. Ellenőrzési feltétel:
minden engedélyezett callback-kimenet után végrehajtódjon az aktív
állapot megszüntetése.
A C callback interfésze függvénytípusból, contextből és viselkedési szerződésből áll. A helyességhez kompatibilis hívást, megfelelő contexttípust, elegendő élettartamot és egyértelmű hibajelzést kell biztosítani. Tárolt regisztrációnál a leiratkozás és a folyamatban lévő hívások befejezése különösen fontos. A cast nem javít típushibát, a nullvizsgálat pedig nem bizonyít érvényességet.