Code That Calls Back: A Complete Guide to Function Callbacks in C

Function callbacks are one of the most powerful features in C programming, enabling dynamic behavior, event handling, and generic programming. A callback is a function that is passed as an argument to another function, allowing the called function to "call back" to the provided code when needed. This pattern is fundamental to GUI programming, asynchronous operations, generic algorithms, and many system libraries.

What Is a Callback?

A callback is simply a function pointer that you pass to another function. The receiving function can then invoke that function pointer at appropriate times—when an event occurs, when data is ready, or as part of an algorithm.

Basic Callback Pattern:
Function A                Function B
(uses callback)           (callback implementation)
|                           |
|---> register callback -----|
|                           |
|                           |
|<--- invoke callback -------|
|                           |

Function Pointer Basics

Before diving into callbacks, we need to understand function pointers:

#include <stdio.h>
// A simple function
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int main() {
// Declare a function pointer
int (*operation)(int, int);
// Point to add function
operation = add;
printf("add(5, 3) = %d\n", operation(5, 3));
// Point to subtract function
operation = subtract;
printf("subtract(5, 3) = %d\n", operation(5, 3));
return 0;
}

Simple Callback Example

#include <stdio.h>
// Callback function types
typedef void (*callback_t)(int);
// Function that accepts a callback
void processNumbers(int start, int end, callback_t callback) {
for (int i = start; i <= end; i++) {
callback(i);  // Call back with each number
}
}
// Callback implementations
void printNumber(int n) {
printf("%d ", n);
}
void printSquare(int n) {
printf("%d ", n * n);
}
void printEvenOdd(int n) {
if (n % 2 == 0) {
printf("%d(even) ", n);
} else {
printf("%d(odd) ", n);
}
}
int main() {
printf("Numbers: ");
processNumbers(1, 5, printNumber);
printf("\n");
printf("Squares: ");
processNumbers(1, 5, printSquare);
printf("\n");
printf("Even/Odd: ");
processNumbers(1, 5, printEvenOdd);
printf("\n");
return 0;
}

Output:

Numbers: 1 2 3 4 5 
Squares: 1 4 9 16 25 
Even/Odd: 1(odd) 2(even) 3(odd) 4(even) 5(odd)

Callbacks with Parameters and Context

Often callbacks need access to additional context or state:

#include <stdio.h>
#include <string.h>
// Callback with context (void* for user data)
typedef void (*callback_with_context_t)(int, void*);
// Processing function with context passing
void processWithContext(int start, int end, 
callback_with_context_t callback, 
void *context) {
for (int i = start; i <= end; i++) {
callback(i, context);
}
}
// Callback that needs context
void printWithPrefix(int n, void *context) {
char *prefix = (char*)context;
printf("%s%d ", prefix, n);
}
void accumulateSum(int n, void *context) {
int *sum = (int*)context;
*sum += n;
}
int main() {
// Using prefix context
char *prefix = "Num: ";
processWithContext(1, 3, printWithPrefix, prefix);
printf("\n");
// Using sum context
int total = 0;
processWithContext(1, 5, accumulateSum, &total);
printf("Sum: %d\n", total);
return 0;
}

Generic Algorithms with Callbacks

The C standard library's qsort is a classic example of callbacks:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Comparison callbacks for qsort
int compareInt(const void *a, const void *b) {
return *(int*)a - *(int*)b;
}
int compareIntDesc(const void *a, const void *b) {
return *(int*)b - *(int*)a;
}
int compareString(const void *a, const void *b) {
return strcmp(*(const char**)a, *(const char**)b);
}
int compareStringLength(const void *a, const void *b) {
return strlen(*(const char**)a) - strlen(*(const char**)b);
}
void printIntArray(int *arr, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
void printStringArray(char **arr, int size) {
for (int i = 0; i < size; i++) {
printf("%s ", arr[i]);
}
printf("\n");
}
int main() {
// Sorting integers
int ints[] = {5, 2, 8, 1, 9, 3, 7, 4, 6};
int intCount = sizeof(ints) / sizeof(ints[0]);
qsort(ints, intCount, sizeof(int), compareInt);
printf("Sorted ascending: ");
printIntArray(ints, intCount);
qsort(ints, intCount, sizeof(int), compareIntDesc);
printf("Sorted descending: ");
printIntArray(ints, intCount);
// Sorting strings
char *strings[] = {"banana", "apple", "cherry", "date", "elderberry"};
int strCount = sizeof(strings) / sizeof(strings[0]);
qsort(strings, strCount, sizeof(char*), compareString);
printf("Sorted strings: ");
printStringArray(strings, strCount);
qsort(strings, strCount, sizeof(char*), compareStringLength);
printf("Sorted by length: ");
printStringArray(strings, strCount);
return 0;
}

Event Handling System

Callbacks are perfect for event-driven programming:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Event types
typedef enum {
EVENT_CLICK,
EVENT_KEYPRESS,
EVENT_MOUSE_MOVE,
EVENT_TIMER
} EventType;
// Event structure
typedef struct {
EventType type;
int x;
int y;
char key;
void *data;
} Event;
// Callback function type
typedef void (*EventHandler)(Event *event, void *user_data);
// Listener structure
typedef struct Listener {
EventType type;
EventHandler handler;
void *user_data;
struct Listener *next;
} Listener;
// Event dispatcher
typedef struct {
Listener *listeners[4];  // One list per event type
} EventDispatcher;
// Initialize dispatcher
void initDispatcher(EventDispatcher *disp) {
for (int i = 0; i < 4; i++) {
disp->listeners[i] = NULL;
}
}
// Register event listener
void addListener(EventDispatcher *disp, EventType type, 
EventHandler handler, void *user_data) {
Listener *newListener = malloc(sizeof(Listener));
newListener->type = type;
newListener->handler = handler;
newListener->user_data = user_data;
newListener->next = disp->listeners[type];
disp->listeners[type] = newListener;
}
// Dispatch event to all listeners
void dispatchEvent(EventDispatcher *disp, Event *event) {
Listener *listener = disp->listeners[event->type];
while (listener) {
listener->handler(event, listener->user_data);
listener = listener->next;
}
}
// Event handler implementations
void onClick(Event *event, void *user_data) {
printf("Click at (%d, %d) - %s\n", 
event->x, event->y, (char*)user_data);
}
void onKeyPress(Event *event, void *user_data) {
printf("Key '%c' pressed - %s\n", 
event->key, (char*)user_data);
}
void onMouseMove(Event *event, void *user_data) {
printf("Mouse moved to (%d, %d) - %s\n", 
event->x, event->y, (char*)user_data);
}
void onTimer(Event *event, void *user_data) {
int *counter = (int*)user_data;
(*counter)++;
printf("Timer tick %d\n", *counter);
}
int main() {
EventDispatcher disp;
initDispatcher(&disp);
// Register various listeners
addListener(&disp, EVENT_CLICK, onClick, "Button1");
addListener(&disp, EVENT_CLICK, onClick, "Button2");
addListener(&disp, EVENT_KEYPRESS, onKeyPress, "Keyboard");
addListener(&disp, EVENT_MOUSE_MOVE, onMouseMove, "Mouse");
int timerCounter = 0;
addListener(&disp, EVENT_TIMER, onTimer, &timerCounter);
// Simulate events
Event e1 = {EVENT_CLICK, 100, 200, 0, NULL};
dispatchEvent(&disp, &e1);
Event e2 = {EVENT_KEYPRESS, 0, 0, 'A', NULL};
dispatchEvent(&disp, &e2);
Event e3 = {EVENT_MOUSE_MOVE, 150, 250, 0, NULL};
dispatchEvent(&disp, &e3);
Event e4 = {EVENT_TIMER, 0, 0, 0, NULL};
dispatchEvent(&disp, &e4);
dispatchEvent(&disp, &e4);
// Cleanup (simplified - would need to free all listeners)
return 0;
}

Callback Chains and Pipelines

Creating a pipeline of operations using callbacks:

#include <stdio.h>
#include <stdlib.h>
// Pipeline stage definition
typedef struct Stage {
void (*process)(int *data, void *context);
void *context;
struct Stage *next;
} Stage;
// Pipeline structure
typedef struct {
Stage *head;
Stage *tail;
} Pipeline;
// Create pipeline
Pipeline* createPipeline() {
Pipeline *p = malloc(sizeof(Pipeline));
p->head = p->tail = NULL;
return p;
}
// Add stage to pipeline
void addStage(Pipeline *p, void (*process)(int*, void*), void *context) {
Stage *s = malloc(sizeof(Stage));
s->process = process;
s->context = context;
s->next = NULL;
if (p->tail) {
p->tail->next = s;
p->tail = s;
} else {
p->head = p->tail = s;
}
}
// Run data through pipeline
void runPipeline(Pipeline *p, int *data) {
Stage *current = p->head;
while (current) {
current->process(data, current->context);
current = current->next;
}
}
// Pipeline stage implementations
void multiplyBy(int *data, void *context) {
int factor = *(int*)context;
*data *= factor;
}
void addValue(int *data, void *context) {
int add = *(int*)context;
*data += add;
}
void clamp(int *data, void *context) {
int *limits = (int*)context;
if (*data < limits[0]) *data = limits[0];
if (*data > limits[1]) *data = limits[1];
}
void printValue(int *data, void *context) {
char *prefix = (char*)context;
printf("%s: %d\n", prefix, *data);
}
int main() {
// Create pipeline
Pipeline *p = createPipeline();
// Add stages
int factor = 2;
addStage(p, multiplyBy, &factor);
int add = 10;
addStage(p, addValue, &add);
int limits[] = {0, 50};
addStage(p, clamp, limits);
addStage(p, printValue, "Result");
// Run data through pipeline
int data[] = {5, 15, 25, 35, 45};
for (int i = 0; i < 5; i++) {
int value = data[i];
runPipeline(p, &value);
}
// Cleanup
Stage *current = p->head;
while (current) {
Stage *temp = current;
current = current->next;
free(temp);
}
free(p);
return 0;
}

Asynchronous Operations with Callbacks

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
// Asynchronous operation structure
typedef struct AsyncOp {
void (*callback)(int result, void *user_data);
void *user_data;
int delay;
} AsyncOp;
// Simulate async operation queue
AsyncOp* createAsyncOp(void (*cb)(int, void*), void *data, int delay) {
AsyncOp *op = malloc(sizeof(AsyncOp));
op->callback = cb;
op->user_data = data;
op->delay = delay;
return op;
}
// Simulate async operation completion
void completeAsyncOp(AsyncOp *op) {
sleep(op->delay);  // Simulate work
int result = rand() % 100;  // Simulate result
op->callback(result, op->user_data);
free(op);
}
// Callback implementations
void onDataLoaded(int result, void *user_data) {
char *filename = (char*)user_data;
printf("Data loaded from %s: %d\n", filename, result);
}
void onNetworkRequest(int result, void *user_data) {
char *url = (char*)user_data;
printf("Network request to %s completed: %d\n", url, result);
}
void onCalculationComplete(int result, void *user_data) {
int *input = (int*)user_data;
printf("Calculation on %d gave result: %d\n", *input, result);
}
int main() {
srand(time(NULL));
printf("Starting async operations...\n");
// Start multiple async operations
AsyncOp *op1 = createAsyncOp(onDataLoaded, "data.txt", 1);
AsyncOp *op2 = createAsyncOp(onNetworkRequest, "api.example.com", 2);
int value = 42;
AsyncOp *op3 = createAsyncOp(onCalculationComplete, &value, 1);
// Simulate completion (in real code, this would be event-driven)
completeAsyncOp(op1);
completeAsyncOp(op2);
completeAsyncOp(op3);
printf("All operations completed\n");
return 0;
}

Callback Registry System

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_CALLBACKS 10
// Callback registry for different events
typedef struct {
char event_name[50];
void (*callback)(void*);
void *user_data;
} CallbackEntry;
typedef struct {
CallbackEntry entries[MAX_CALLBACKS];
int count;
} CallbackRegistry;
// Initialize registry
void initRegistry(CallbackRegistry *reg) {
reg->count = 0;
}
// Register callback
int registerCallback(CallbackRegistry *reg, const char *event, 
void (*callback)(void*), void *user_data) {
if (reg->count >= MAX_CALLBACKS) return -1;
CallbackEntry *entry = &reg->entries[reg->count++];
strcpy(entry->event_name, event);
entry->callback = callback;
entry->user_data = user_data;
return 0;
}
// Trigger event
void triggerEvent(CallbackRegistry *reg, const char *event) {
printf("\n=== Event: %s ===\n", event);
for (int i = 0; i < reg->count; i++) {
if (strcmp(reg->entries[i].event_name, event) == 0) {
reg->entries[i].callback(reg->entries[i].user_data);
}
}
}
// Callback implementations
void onStartup(void *data) {
char *app = (char*)data;
printf("%s: Starting up...\n", app);
}
void onShutdown(void *data) {
char *app = (char*)data;
printf("%s: Shutting down...\n", app);
}
void onSave(void *data) {
char *filename = (char*)data;
printf("Saving to %s...\n", filename);
}
void onTimer(void *data) {
int *count = (int*)data;
(*count)++;
printf("Timer tick %d\n", *count);
}
int main() {
CallbackRegistry reg;
initRegistry(&reg);
// Register callbacks
registerCallback(&reg, "startup", onStartup, "App1");
registerCallback(&reg, "startup", onStartup, "App2");
registerCallback(&reg, "shutdown", onShutdown, "App1");
registerCallback(&reg, "save", onSave, "data.txt");
int timerCount = 0;
registerCallback(&reg, "timer", onTimer, &timerCount);
// Trigger events
triggerEvent(&reg, "startup");
triggerEvent(&reg, "timer");
triggerEvent(&reg, "timer");
triggerEvent(&reg, "save");
triggerEvent(&reg, "shutdown");
return 0;
}

Filter and Map with Callbacks

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Map function (apply callback to each element)
void map(int *array, int size, int (*func)(int)) {
for (int i = 0; i < size; i++) {
array[i] = func(array[i]);
}
}
// Filter function (keep elements where callback returns true)
int filter(int *array, int size, int (*predicate)(int), int *result) {
int count = 0;
for (int i = 0; i < size; i++) {
if (predicate(array[i])) {
result[count++] = array[i];
}
}
return count;
}
// Reduce function (accumulate using callback)
int reduce(int *array, int size, int (*func)(int, int), int initial) {
int result = initial;
for (int i = 0; i < size; i++) {
result = func(result, array[i]);
}
return result;
}
// Map operations
int square(int x) { return x * x; }
int doubleIt(int x) { return x * 2; }
int negate(int x) { return -x; }
// Predicates for filter
int isEven(int x) { return x % 2 == 0; }
int isPositive(int x) { return x > 0; }
int isGreaterThan10(int x) { return x > 10; }
// Reduce operations
int sum(int acc, int x) { return acc + x; }
int max(int acc, int x) { return x > acc ? x : acc; }
int product(int acc, int x) { return acc * x; }
void printArray(int *arr, int size, const char *name) {
printf("%s: ", name);
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int numbers[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int size = sizeof(numbers) / sizeof(numbers[0]);
printArray(numbers, size, "Original");
// Map operations
map(numbers, size, square);
printArray(numbers, size, "After square");
map(numbers, size, doubleIt);
printArray(numbers, size, "After double");
// Filter operations
int filtered[10];
int filteredCount = filter(numbers, size, isEven, filtered);
printArray(filtered, filteredCount, "Even numbers");
// Reduce operations
int total = reduce(numbers, size, sum, 0);
printf("Sum: %d\n", total);
int maximum = reduce(numbers, size, max, numbers[0]);
printf("Max: %d\n", maximum);
return 0;
}

Callback with Multiple Parameters Using Structs

#include <stdio.h>
#include <stdlib.h>
// Parameter structure for complex callbacks
typedef struct {
int x;
int y;
char op;
void *user_data;
} CalcParams;
// Callback type
typedef void (*ComplexCallback)(CalcParams *params, int result);
// Calculator function
void calculate(int a, int b, char op, ComplexCallback callback, void *user_data) {
int result;
switch (op) {
case '+': result = a + b; break;
case '-': result = a - b; break;
case '*': result = a * b; break;
case '/': 
if (b != 0) result = a / b;
else result = 0;
break;
default: result = 0;
}
CalcParams params = {a, b, op, user_data};
callback(&params, result);
}
// Callback implementations
void logResult(CalcParams *params, int result) {
printf("Calculation: %d %c %d = %d\n", 
params->x, params->op, params->y, result);
}
void storeResult(CalcParams *params, int result) {
int *storage = (int*)params->user_data;
*storage = result;
printf("Stored result %d\n", result);
}
void formatResult(CalcParams *params, int result) {
char *buffer = (char*)params->user_data;
sprintf(buffer, "%d %c %d = %d", 
params->x, params->op, params->y, result);
}
int main() {
// Simple logging callback
calculate(10, 5, '+', logResult, NULL);
calculate(10, 5, '-', logResult, NULL);
calculate(10, 5, '*', logResult, NULL);
// Store result in variable
int stored;
calculate(20, 4, '/', storeResult, &stored);
printf("Stored value: %d\n", stored);
// Format result into string
char formatted[100];
calculate(15, 3, '*', formatResult, formatted);
printf("Formatted: %s\n", formatted);
return 0;
}

Callback Error Handling

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
// Error callback type
typedef void (*ErrorCallback)(int error_code, const char *message, void *context);
// Operation that might fail
int riskyOperation(int input, ErrorCallback onError, void *context) {
if (input < 0) {
onError(EINVAL, "Input cannot be negative", context);
return -1;
}
if (input > 100) {
onError(ERANGE, "Input exceeds maximum", context);
return -1;
}
if (input == 42) {
onError(0, "Warning: 42 is special", context);  // Warning, not error
}
return input * 2;
}
// Error handlers
void logError(int code, const char *msg, void *context) {
FILE *log = (FILE*)context;
fprintf(log, "Error %d: %s\n", code, msg);
}
void printError(int code, const char *msg, void *context) {
printf("ERROR: %s (code %d)\n", msg, code);
}
void countError(int code, const char *msg, void *context) {
int *counter = (int*)context;
(*counter)++;
printf("Error %d occurred (total: %d)\n", code, *counter);
}
int main() {
// Test with different error handlers
FILE *log = fopen("errors.log", "w");
int result;
result = riskyOperation(50, logError, log);
printf("Result: %d\n\n", result);
result = riskyOperation(-5, printError, NULL);
printf("Result: %d\n\n", result);
int errorCount = 0;
result = riskyOperation(150, countError, &errorCount);
printf("Result: %d\n", result);
result = riskyOperation(-10, countError, &errorCount);
printf("Result: %d\n", result);
printf("Total errors: %d\n\n", errorCount);
result = riskyOperation(42, printError, NULL);  // Warning
printf("Result: %d\n", result);
fclose(log);
return 0;
}

Callback Performance Considerations

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// Direct function call
int addDirect(int a, int b) {
return a + b;
}
// Function to be called via callback
int addOperation(int a, int b) {
return a + b;
}
// Callback type
typedef int (*binary_op_t)(int, int);
// Function using callback
int applyCallback(int a, int b, binary_op_t op) {
return op(a, b);
}
#define ITERATIONS 10000000
int main() {
clock_t start, end;
volatile int result;  // Prevent optimization
// Benchmark direct call
start = clock();
for (int i = 0; i < ITERATIONS; i++) {
result = addDirect(i, i + 1);
}
end = clock();
double direct_time = ((double)(end - start)) / CLOCKS_PER_SEC;
// Benchmark callback
start = clock();
for (int i = 0; i < ITERATIONS; i++) {
result = applyCallback(i, i + 1, addOperation);
}
end = clock();
double callback_time = ((double)(end - start)) / CLOCKS_PER_SEC;
printf("Direct call:  %.3f seconds\n", direct_time);
printf("Callback:     %.3f seconds\n", callback_time);
printf("Overhead:     %.2f%%\n", 
(callback_time - direct_time) / direct_time * 100);
return 0;
}

Common Callback Patterns

1. Comparison Callbacks (like qsort)

typedef int (*comparator_t)(const void*, const void*);

2. Iteration Callbacks

typedef void (*iterator_t)(void *item, void *context);

3. Predicate Callbacks (filtering)

typedef int (*predicate_t)(const void *item);

4. Transformation Callbacks

typedef void (*transform_t)(void *input, void *output, void *context);

5. Cleanup/Destructor Callbacks

typedef void (*destructor_t)(void *data);

Best Practices

  1. Always check for NULL function pointers before calling
  2. Use typedef for callback types to improve readability
  3. Provide context/void* parameters to make callbacks stateful
  4. Document callback expectations (when called, what parameters mean)
  5. Consider error handling in callback chains
  6. Be mindful of performance (callback overhead vs. flexibility trade-off)
  7. Avoid deep callback nesting (callback hell)
  8. Use const for read-only parameters in callbacks

Common Pitfalls

// Pitfall 1: Not checking for NULL callback
void badFunction(callback_t cb) {
cb(42);  // Crash if cb is NULL!
}
// Pitfall 2: Calling callback after freeing its context
void* context = malloc(...);
register_callback(callback, context);
free(context);
// Later, callback called with freed context!
// Pitfall 3: Recursive callbacks causing stack overflow
void recursive_callback(int n, void *cb) {
if (n > 0) {
((void (*)(int, void*))cb)(n-1, cb);  // Recursive call through callback
}
}
// Pitfall 4: Type mismatches in callback signatures
int (*int_cb)(int) = (int (*)(int))some_callback;  // Dangerous cast!
// Pitfall 5: Not handling callback errors

Callback Design Patterns Cheat Sheet

PatternDescriptionUse Case
Event ListenerMultiple callbacks for eventsGUI, messaging
StrategyDifferent algorithms via callbacksSorting, searching
ObserverNotify interested partiesState changes
Chain of ResponsibilityPipeline of callbacksData processing
CommandEncapsulate operationUndo/redo
VisitorTraverse structuresTree/Graph operations

Conclusion

Function callbacks are a fundamental building block of flexible C programming. They enable:

  • Generic algorithms that work with any data type
  • Event-driven programming for interactive applications
  • Asynchronous operations without blocking
  • Plugin architectures with runtime behavior customization
  • Decoupled code with separation of concerns

Key principles to remember:

  • Callbacks are just function pointers passed as arguments
  • Always include a void* context parameter for state
  • Document callback signatures clearly
  • Handle errors appropriately in callback chains
  • Consider performance implications in tight loops

Mastering callbacks opens up powerful programming paradigms and is essential for working with many C libraries and frameworks. They provide the flexibility and extensibility that makes C suitable for everything from embedded systems to large-scale applications.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper