Remix.run Logo
sltkr 5 hours ago

For the non-capturing case: mainly to improve readability by allowing utility functions to be defined close to where they are used and with short names.

For the capturing case: to access context that is not available through global variables or function arguments, i.e., the same reason why closures are useful in other languages.

Here's an example, where I have a list of points that I want to sort based on distance to a chosen target point. I can use qsort() which takes an arbitrary comparison function, but has no way to provide context to that function beyond the input arguments:

    #include <stdio.h>
    #include <stdlib.h>

    int main() {
        struct Point {
            int x, y;
        } points[3] = {
            { 3, 1 },
            { 2, 2 },
            { 5, 7 } };

        struct Point target = { 4, 5 };

        long dsq(const struct Point *p) {
            long dx = p->x - target.x, dy = p->y - target.y;
            return dx*dx + dy*dy;
        }

        int compare(const void *p, const void *q) {
            long a = dsq(p), b = dsq(q);
            return (a > b) - (a < b);
        }

        qsort(points, 3, sizeof(struct Point), compare);

        for (int i = 0; i < 3; ++i) {
            printf("%d,%d\n", points[i].x, points[i].y);
        }
    }
Note here that dsq() is a local function that accesses the `target` variable in the local function scope.

The usual workaround in standard C is to pass the necessary context as a function argument. That's why qsort_r() exists, which takes a context argument to be passed to compare(), but that's a non-standard GNU extension.

This practice of passing context pointers around is ubiquitous in C code, and it works, but it can get messy especially if you need access to multiple variables or variables from more than one nested scope. There is also a type safety issue: these context pointers are necessarily passed as void* which means they have to be cast back to the real type before use, which is where bugs can be introduced if the caller and receiver disagree on the actual type.

uecker 3 hours ago | parent [-]

This is a good example. Without trampolines, this could look like this (Godbolt: https://godbolt.org/z/nK5fqMxjs).

    int main() {
        struct Point {
            int x, y;
        } points[3] = {
            { 3, 1 },
            { 2, 2 },
            { 5, 7 }
        };

        struct Point target = { 4, 5 };

        long dsq(const struct Point *p) {
            long dx = p->x - target.x, dy = p->y - target.y;
            return dx*dx + dy*dy;
        }

        typedef typeof(dsq) dsq_f;

        int compare(const void *p, const void *q, void *data) {
           wide(dsq_f) *dsq = data;
           long a = CALL(*dsq, (p)), b = CALL(*dsq, (q));
           return (a > b) - (a < b);
        }

        qsort_r(points, 3, sizeof(struct Point), compare, &CLOSURE(dsq_f, dsq));

        for (int i = 0; i < 3; ++i)
            printf("%d,%d\n", points[i].x, points[i].y);
    }
There are slightly different ways how to define the helper macros, I am still experimenting a bit. Here you could avoid the typedef if defined differently. But ideally, there would be native language support that avoids these macros.
listeria 2 hours ago | parent | next [-]

Well, if you're already using qsort_r, what's the point of using nested functions, if you can have a context pointer with the target?

And if you're not using qsort_r, but reaching for _Thread_local, the target can be _Thread_local instead of dsq.

uecker 2 hours ago | parent [-]

Fair. If you only have one object to access such as the target pointer, then it probably makes not much difference with void-pointer based APIs such as qsort_r (for new APIs it would add type safety). Where it removes more boilerplate code is when you have several such objects and would have to create an extra data structure to access them.

uecker 3 hours ago | parent | prev [-]

Or without qsort_r, you could use a thread local variable:

    typedef typeof(dsq) dsq_f;
    _Thread_local static wide(dsq_f) wdsq;
    wdsq = CLOSURE(dsq_f, dsq);
https://godbolt.org/z/3e157c6b1