\

Using GCC's Nested Functions with Wide Pointers and No Trampolines II

38 points - today at 7:59 AM

Source
  • mbeavitt

    today at 11:50 AM

    Why would someone want to use a nested function, practically speaking?

      • mananaysiempre

        today at 12:16 PM

        Good C style is that every function that accepts a callback should also accept an opaque context pointer it then passes through unchanged to the callback. Usually the caller will allocate a structure on the stack or the heap, stash some of its local variables there, then use them in the callback. A nested function does the structure back-and-forth for you in the stack-allocated case. In GCC’s original formulation it also passes the context pointer implicitly

          size_t filter(bool (*predicate)(int), int *p, size_t n) {
              for (size_t r = 0, w = 0; r < n; r++) {
                  if (predicate(p[r])) p[w++] = p[r];
              }
              return w;
          }
          size_t lowpass(int limit, int *p, size_t n) {
              bool lower(int value) {
                  return value < limit; // use the parent's local variable
              }
              return filter(lower, p, n);
          }
        
        but that requires an executable stack and TFA is about avoiding that part.

        • psyclobe

          today at 1:01 PM

          RAII style cleanup e.g. no gotos

          • anta40

            today at 12:04 PM

            Say to strictly enforce modularity, e.g helper functions that can only be accessed within its function.

            Pascal supports it (at least Turbo Pascal, no idea about ISO Pascal).

            • sltkr

              today at 12:27 PM

              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.

              • jcranmer

                today at 12:15 PM

                When you want to use lambdas, but your language doesn't have lambdas, so you reach for the nearest thing instead.

                  • mananaysiempre

                    today at 12:33 PM

                    C++ bundles together a way to write functions inline in an expression (what I’d call “lambdas” in general) and a way to create closures with strictly nested lifetimes, but there’s no law of nature tying the two together. Even in C++ code “auto f = [&](... blah ...) { ... 50 lines of code ... };” is pretty common. (And of course GCC’s nested functions predate C++11 by twenty years.)

                • bobmcnamara

                  today at 12:23 PM

                  Just a little cleaner than placing it in the global or file namespaces.

                  • kloop

                    today at 11:55 AM

                    So that you can name a section of code without polluting the namespace.

                • tpoacher

                  today at 11:42 AM

                  What's a "trampoline"?

                    • jcranmer

                      today at 12:20 PM

                      In this context:

                      Nested functions have a different ABI from regular C functions, due to the invisible static chain register that needs to be set up. C has no way of indicating this different ABI, so GCC happily lets you cast a nested function to a C function pointer by creating a little tiny function that puts the right value in the static chain register before calling the nested function. This little tiny function is the trampoline.

                      Since the trampoline needs to live somewhere, GCC puts it on the stack, requiring the stack to be executable and consequently a whole lot of people hate the feature because it's a walking security nightmare.

                      • mananaysiempre

                        today at 11:58 AM

                        Could be a number of things depending on context. In this case it’s a short function that adjusts some things and jumps to the actual functions (a “thunk” is another term for this). Specifically, if in GCC you write

                          int f(int x) {
                              int g(int y) { ... use x and y ... }
                              ...
                              h(&g);
                              ...
                          }
                        
                        then what the compiled code for f does is construct on the stack a short piece of machine code:

                          mov <well-known register>, <frame pointer>
                          jmp <start of g’s code>
                        
                        and &g points to the start not of g’s code but of this snippet on the stack, which has the parent function’s frame pointer compiled into it as a literal constant. The snippet is called a trampoline.

                        • monster_truck

                          today at 11:49 AM

                          It's where you jump and then get immediately bounced back. Basically GOTOs with params

                      • mananaysiempre

                        today at 10:55 AM

                        What about your older patch where -fno-trampolines meant a function pointer could either be a code pointer or a closure (descriptor) pointer, distinguished by a tag?

                          • uecker

                            today at 11:40 AM

                            My old patch from 2018? This was not accepted to GCC because it relied on function pointers being aligned and there were concerns with this.

                            But I prefer this approach anyhow, as it does not impose any run-time cost for checking the tag, and is easier to optimize.