C / POINTERS
Callbacks with function pointers and void* context
Pass behaviour into a function as a function pointer paired with a void* context, then cast that context back inside the callback to keep state per call.
What you will learn
- Declare a callback parameter correctly: void (*fn)(int, void *), parentheses included
- Thread one void* context through a generic function and cast it back in the callback
- Replace globals with a context struct so two walks can run with independent state
- Keep the context object alive for as long as the callback can still be invoked
Understanding Callbacks with function pointers and void* context
A function name used in an expression converts to a pointer to that function, so sum and &sum are the same value, and a callback is nothing more exotic than handing that value to someone else so they can call it. When you declare the receiving parameter, the parentheses around the star are load-bearing: void (*visit)(int, void *) is a pointer to a function returning void, while void *visit(int, void *) declares a function returning void *. Calling it is plain visit(x, ctx); the older (*visit)(x, ctx) also works, because the dereference yields a function designator that immediately converts back to a pointer, so both spellings compile to the same call.
C has no closures, so a function pointer carries code and nothing else: no captured variables ride along with it. Anything the callback needs beyond its own arguments must arrive through a parameter, and since the generic function cannot know your types, that parameter is a void * it forwards untouched. That single pointer is your manual capture list — put the threshold, the accumulator and the output buffer into one struct and pass its address. Because the state now lives in the caller's struct instead of a file-scope variable, the same walker can run twice with two different contexts, recursively or on two threads, without the runs corrupting each other.
Three parties share the arrangement and only two of them know anything: the caller owns the context object and its type, the callback casts the void * back to that type, and the code in between only knows it has an address to forward. The compiler cannot check that middle step, because void * converts to any object pointer silently, so a wrong cast gives you garbage fields rather than a diagnostic — keep the callback and the context paired at a single call site so they change together. Lifetime is the other unchecked half: a local struct is fine while a synchronous loop runs, but a callback stored for later must never point into a stack frame that has already returned.
<stdio.h>
<stddef.h>
typedef void (*IntVisitor)(int value, void *ctx);
static void for_each_int(const int *a, size_t n, IntVisitor visit, void *ctx)
{
for (size_t i = 0; i < n; i++)
visit(a[i], ctx); /* ctx is forwarded, never inspected */
}
struct sum_ctx { long total; };
static void add_to_total(int value, void *ctx)
{
struct sum_ctx *s = ctx; /* the one place the real type is known */
s->total += value;
}
struct count_ctx { int limit; int hits; };
static void count_above(int value, void *ctx)
{
struct count_ctx *c = ctx;
if (value > c->limit)
c->hits++;
}
int main(void)
{
int data[] = { 4, 17, -3, 40, 8 };
size_t n = sizeof data / sizeof data[0];
struct sum_ctx s = { 0 };
for_each_int(data, n, add_to_total, &s);
printf("total = %ld\n", s.total);
struct count_ctx c = { 10, 0 };
for_each_int(data, n, count_above, &c);
printf("%d values above %d\n", c.hits, c.limit);
return 0;
}
A callback is a function pointer for the behaviour plus a void* context for the state, because C function pointers capture nothing on their own.
Worked examples
qsort: a callback with no context slot
Shows the double indirection a qsort comparator needs, and the fact that its fixed signature leaves no room for a void* context.
<stdio.h>
<stdlib.h>
<string.h>
static int by_length_desc(const void *a, const void *b)
{
const char *sa = *(const char *const *)a; /* element is itself a pointer */
const char *sb = *(const char *const *)b;
size_t la = strlen(sa), lb = strlen(sb);
if (la != lb)
return (la < lb) - (la > lb); /* longer string first */
return strcmp(sa, sb);
}
int main(void)
{
const char *words[] = { "pointer", "int", "callback", "void", "cast" };
size_t n = sizeof words / sizeof words[0];
qsort(words, n, sizeof words[0], by_length_desc);
for (size_t i = 0; i < n; i++)
printf("%s\n", words[i]);
return 0;
}
Example explained
Line 1a and b point at array elements, and each element is a const char *, so you cast to const char *const * and dereference once to reach the string.
Line 2(la < lb) - (la > lb) returns -1, 0 or 1 without computing la - lb, which would wrap because size_t is unsigned.
Line 3The comparator has only two parameters, so a sort key such as a minimum length cannot be passed in; plain qsort forces you into a file-scope variable.
Line 4The context-passing variants are qsort_r (glibc and BSD, with different argument orders) and the optional qsort_s from C11 Annex K.
Early exit and results returned through the context
A callback whose return value controls the loop while its findings travel back in the caller's struct.
<stdio.h>
<string.h>
struct find_ctx { const char *needle; int index; };
static int check(int idx, const char *name, void *ctx)
{
struct find_ctx *f = ctx;
if (strcmp(name, f->needle) == 0) {
f->index = idx;
return 0; /* 0 means stop */
}
return 1; /* 1 means keep going */
}
static void walk(const char **names, int n,
int (*fn)(int, const char *, void *), void *ctx)
{
for (int i = 0; i < n; i++)
if (!fn(i, names[i], ctx))
return;
}
int main(void)
{
const char *names[] = { "ada", "grace", "alan", "edsger" };
struct find_ctx f = { "alan", -1 };
walk(names, 4, check, &f);
printf("index of %s: %d\n", f.needle, f.index);
struct find_ctx g = { "linus", -1 };
walk(names, 4, check, &g);
printf("index of %s: %d\n", g.needle, g.index);
return 0;
}
Example explained
Line 1The return value is a documented protocol, not a result: walk owns the loop, the callback owns the decision to continue.
Line 2The answer comes back in f.index rather than from walk, which is what lets walk stay ignorant of what the callback is computing.
Line 3Both calls use the identical function pointer with different contexts, so the -1 sentinel in g is untouched by the first search.
Line 4The needle lives in the same struct as the result, so no global is needed to tell the callback what to look for.
Important notes
The C standard only guarantees round-tripping object pointers through void *, not function pointers, so keep callbacks in a function-pointer type or typedef and never stash one in a void *.
A context of NULL is a legitimate choice when the callback needs no state, but the callback must then be written to expect it rather than dereferencing blindly.
Common mistakes
Writing the parameter as void *visit(int, void *), which declares a function returning void * rather than a pointer to a function returning void; passing your handler then draws an incompatible-pointer-type error that looks unrelated to the missing parentheses.
Copy-pasting a callback and forgetting to change the cast, so a struct count_ctx * is read as a struct sum_ctx *; void* converts silently, so there is no warning and you get nonsense values or a corrupted neighbouring field.
Registering a callback with &local from a function that then returns, leaving the stored void* aimed at a dead stack frame; the later call reads recycled memory and fails intermittently, usually far from the registration site.
Try it yourself
Change, predict, then run
Write int for_each_until(const int *a, size_t n, int (*fn)(int, void *), void *ctx) that stops as soon as the callback returns 0, and drive it with a context holding a running product plus a cap, so the walk stops once the product passes the cap. Print the product and how many elements were consumed.
Open the C workspaceCheck your understanding
Inside setup(), you write struct cfg c = { .level = 2 }; log_set_handler(on_log, &c); and setup() returns. The logging library later calls on_log from its own thread. What is wrong?
- The stored context points into setup's frame, which no longer exists, so on_log reads reclaimed memory
- Nothing: because the library receives a void*, it copies the object it points to
- It will not compile, since &c is a struct pointer and the parameter is void*
- on_log receives NULL, because a void* cannot carry the address of a struct
Show answer
A void* is only an address; the library knows neither the type nor the size of *ctx, so it cannot copy the struct even in principle, and passing &c transfers no lifetime guarantee. Option 2 is tempting because the pointer looks opaque and self-contained, but that opacity is exactly why no copy can happen. Option 3 is wrong because any object pointer converts to void* implicitly, and option 4 misreads void* as a type restriction rather than a type erasure.