Callback adalah fungsi yang kita kirimkan ke fungsi lain agar dipanggil kembali pada saat tertentu. Pola ini mengubah hubungan pemanggil–pemanggilan: pihak pemanggil tidak lagi menentukan apa yang terjadi, melainkan kapan tindakan terjadi; tindakan konkretnya disuntikkan dari luar. Konsep ini menjadi tulang punggung pustaka populer seperti qsort (comparator), event loop GUI, dan sistem logging. Pada chapter ini kita akan membangun dua API berbasis callback: for_each() yang menerapkan inversion of control pada iterasi array, serta sistem event sederhana dengan banyak listener.

Callback untuk Iterasi: for_each #

Bayangkan kita punya array int dan ingin menjalankan tindakan berbeda untuk setiap elemen: mencetak, mengakumulasi total, atau menghitung elemen genap. Tanpa callback, kita akan menulis tiga loop for yang strukturnya identik. Dengan callback, kita menulis loop sekali, lalu menyuntikkan tindakan.

c
#include <stdio.h>

/* Tipe callback: menerima elemen dan indeksnya */
typedef void (*ElemFn)(int elem, int idx);

/* Iterasi array, panggil callback untuk setiap elemen */
void for_each(const int *arr, int n, ElemFn cb) {
    for (int i = 0; i < n; i++) {
        cb(arr[i], i);
    }
}

/* Callback: cetak elemen */
static void cetak_elemen(int elem, int idx) {
    printf("arr[%d] = %d\n", idx, elem);
}

/* Callback: akumulasi total via variabel global demo sederhana */
static int total = 0;
static void akumulasi(int elem, int idx) {
    (void)idx;
    total += elem;
}

/* Callback: catat elemen genap */
static int jumlah_genap = 0;
static void hitung_genap(int elem, int idx) {
    (void)idx;
    if (elem % 2 == 0) jumlah_genap++;
}

int main(void) {
    int arr[] = {10, 21, 32, 43, 54};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("== cetak ==\n");
    for_each(arr, n, cetak_elemen);

    printf("== akumulasi ==\n");
    for_each(arr, n, akumulasi);
    printf("total = %d\n", total);

    printf("== genap ==\n");
    for_each(arr, n, hitung_genap);
    printf("jumlah genap = %d\n", jumlah_genap);

    return 0;
}

Output yang Dihasilkan:

text
== cetak ==
arr[0] = 10
arr[1] = 21
arr[2] = 32
arr[3] = 43
arr[4] = 54
== akumulasi ==
total = 160
== genap ==
jumlah genap = 3

Mari kita telaah baris-baris kode di atas:

  1. Baris 5 (typedef void (*ElemFn)(int elem, int idx);): alias untuk tipe “pointer ke fungsi yang menerima dua int dan mengembalikan void”. Memberi nama pada tipe function pointer membuat signature for_each jauh lebih mudah dibaca.
  2. Baris 8-12 (void for_each(const int *arr, int n, ElemFn cb)): fungsi generik yang hanya tahu cara iterasi. Tindakan apa yang dilakukan pada setiap elemen ditentukan oleh cb yang dipanggil pemanggil. Inilah inti inversion of control.
  3. Baris 15-17 (cetak_elemen): callback konkrit yang mencetak elemen beserta indeksnya. Signature harus persis cocok dengan ElemFn.
  4. Baris 21-24 (akumulasi): callback ini mengakumulasi ke variabel total. Pola variabel global di sini hanya untuk demo—di kode produksi, lebih baik mengoper context pointer sebagai parameter tambahan agar callback reentrant.
  5. Baris 28-32 (hitung_genap): callback yang memeriksa kondisi tiap elemen dan mengubah penghitung. Satu mesin iterasi, tiga perilaku berbeda.

Sistem Event dengan Banyak Listener #

Event-driven programming memungkinkan satu peristiwa menyalakan banyak reaksi tanpa pengirim tahu daftar reaksi tersebut. Kita bangun registry sederhana: fungsi on() mendaftar listener, emit() memanggil semua listener untuk event tertentu.

c
#include <stdio.h>

typedef enum {
    EVT_LOGIN,
    EVT_LOGOUT,
    EVT_BELI
} EventType;

typedef struct {
    EventType type;
    const char *user;
    int data;
} Event;

/* Tipe callback event */
typedef void (*EventListener)(const Event *);

#define MAX_LISTENERS 8

static EventListener listeners[MAX_LISTENERS];
static int listener_count = 0;

void on(EventListener cb) {
    if (listener_count < MAX_LISTENERS) {
        listeners[listener_count++] = cb;
    }
}

void emit(const Event *e) {
    for (int i = 0; i < listener_count; i++) {
        listeners[i](e);
    }
}

/* Listener: logger */
static void logger_listener(const Event *e) {
    const char *nama = "?";
    switch (e->type) {
        case EVT_LOGIN:  nama = "LOGIN"; break;
        case EVT_LOGOUT: nama = "LOGOUT"; break;
        case EVT_BELI:   nama = "BELI"; break;
    }
    printf("[log] %s user=%s data=%d\n", nama, e->user, e->data);
}

/* Listener: notifier belanja */
static void beli_listener(const Event *e) {
    if (e->type == EVT_BELI) {
        printf("[notifikasi] Terima kasih, %s, pembelian %d tercatat\n",
               e->user, e->data);
    }
}

/* Listener: keamanan */
static void keamanan_listener(const Event *e) {
    if (e->type == EVT_LOGIN && e->data == 0) {
        printf("[keamanan] login gagal untuk user=%s\n", e->user);
    }
}

int main(void) {
    on(logger_listener);
    on(beli_listener);
    on(keamanan_listener);

    Event events[] = {
        {EVT_LOGIN,  "budi", 1},
        {EVT_BELI,   "budi", 75000},
        {EVT_LOGIN,  "siti", 0},
        {EVT_LOGOUT, "budi", 0},
    };
    int n = sizeof(events) / sizeof(events[0]);

    for (int i = 0; i < n; i++) {
        emit(&events[i]);
    }
    return 0;
}

Output yang Dihasilkan:

text
[log] LOGIN user=budi data=1
[log] BELI user=budi data=75000
[notifikasi] Terima kasih, budi, pembelian 75000 tercatat
[log] LOGIN user=siti data=0
[keamanan] login gagal untuk user=siti
[log] LOGOUT user=budi data=0

Perhatikan satu event EVT_BELI memicu dua listener: logger_listener mencatat log, beli_listener mengirim notifikasi terima kasih. Pengirim event (emit()) tidak perlu tahu listener apa saja yang terdaftar—pemisahan tanggung jawab ini membuat kode mudah diperluas tanpa menyentuh kode pengirim.

Jebakan Umum Callback #

  • Reentrancy: bila callback memanggil kembali API yang sama (misalnya on() di dalam listener), urutan listener bisa berubah saat iterasi sedang berjalan. Solusinya: salin dulu daftar listener ke array lokal sebelum iterasi.
  • Use-after-free: bila listener membebaskan objek yang menjadi sumber event, listener lain yang belum dipanggil akan menerima pointer dangling. Hindari mengubah state registry di dalam callback.
  • Exception di callback: C tidak punya exception, tetapi setjmp/longjmp dari dalam callback akan melewati cleanup listener berikutnya. Sediakan kode error lewat return value atau output parameter.