attribute((cleanup)), mixed declarations and code, and goto.

One of the cool features of recent GLib is g_autoptr() and g_autofree. It’s liberating to be able to write:

g_autofree char *filename = g_strdup_printf("%s/%d.txt", dir, count);

And be sure that will be freed no matter how your function returns. But as I started to use it, I realized that I wasn’t very sure about some details about the behavior, especially when combined with mixing declarations and code as allowed by C99.

Internally g_autofree uses __attribute__((cleanup)), which is supported by GCC and clang. The definition of g_autofree is basically:

static inline void
g_autoptr_cleanup_generic_gfree (void *p)
{
  void **pp = (void**)p;
  g_free (*pp);
}

#define g_autofree __attribute__((cleanup(g_autoptr_cleanup_generic_gfree)))

Look at the following examples:

int count1(int arg)
{
  g_autofree char *str;

  if (arg < 0)
    return -1;

  str = g_strdup_printf("%d", arg);

  return strlen(str);
}


int count2(int arg)
{
  if (arg < 0)
    return -1;

  g_autofree char *str = g_strdup_printf("%d", arg);

  return strlen(str);
}

int count3(int arg)
{
  if (arg < 0)
    goto out;

  g_autofree char *str = g_strdup_printf("%d", arg);

  return strlen(str);

out:
  return -1;
}

int count4(int arg)
{
  if (arg < 0)
    goto out;

  {
    g_autofree char *str = g_strdup_printf("%d", arg);

    return strlen(str);
  }

out:
  return 0;
}

Which ones of these do you think work as intended, and which ones are buggy? (I’m not recommending this as a way counting the digits in a number – the example is artificial.)

count1() is pretty clearly buggy – the cleanup function will run in the error
path and try to free an uninitialized string. Slightly, more subtly, count3() is also buggy – because the goto jumps over the initialization. But count2() and count4() work as intended.

To understand why this is the case, it’s worth looking at how attribute((cleanup)) is described in the GCC manual – all it says is “the ‘cleanup’ attribute runs a function when the variable goes out of scope.” I first thought that this was a completely insufficient definition – not complete enough to allow figuring out what was supposed to happen in the above cases, but thinking about it a bit, it’s actually a precise definition.

To recall, the scope of a variable in C is from the point of the declaration of the variable to the end of the enclosing block. What the definition is saying is that any time a variable is in scope, and then goes out of scope, there is an implicit call to the cleanup function.

In the early return in count1() and at the return that is jumped to in count3(), the variable ‘str’ is in scope, so the cleanup function will be called, even though the variable is not initialized in either case. In the corresponding places in count2() and count4() the variable ‘str’ is not in scope, so the cleanup function will not be called.

The coding style takewaways from this are 1) Don’t use the g_auto* attributes on a variable that is not initialzed at the time of definition 2) be very careful if combining goto with g_auto*.

It should be noted that GCC is quite good at warning about it if you get it wrong, but it’s still better to understand the rules and get it right from the start.

7 Comments

  1. Nathaniel McCallum
    Posted November 5, 2015 at 9:14 pm | Permalink

    For the curious, it *does* work when mixed with C++ exceptions. An exception that jumps over a scope will successfully cause the cleanup callback to be executed.

  2. xclaesse
    Posted November 6, 2015 at 9:44 am | Permalink

    Good to know, you should add those examples in g_autoptr’s doc IMHO.

  3. Posted November 6, 2015 at 3:37 pm | Permalink

    Is there a way to check or warn about this? Perhaps a clang plugin or some such hackery?

    • Owen
      Posted November 7, 2015 at 12:42 am | Permalink

      GCC is pretty good at warning at it – at least as GLib uses attribute((cleanup)) with the cleanup functions being inline functions. I honestly haven’t seen it miss anything problematical, though I’m sure such cases exist.

      You can enforce the C++ rule of no-jumps-over-initialization with -Werror=jump-misses-init, though that prohibits all initializers, including integers, etc, and thus prohibits some harmless code.

      clang, in a quick test, seems to enforce that you can’t jump over an initialization with a cleanup function out of the box – which is exactly what you’d want. I don’t think that can ever be correct.

      So the necessary clang plugin seems to be the empty plugin 🙂

  4. Tristian Celestin
    Posted November 6, 2015 at 10:57 pm | Permalink

    I don’t understand why count3() segfaults. My expectation was that execution of the line

    goto out

    would skip over the declaration and initialization of str. What causes str to be in scope if the goto skips its declaration?

    • Owen
      Posted November 7, 2015 at 12:48 am | Permalink

      You probably wouldn’t be surprised that the following code works:

       if (x < 0)
         goto label1;
      
       int a = x + 1;
       return a * a;
      label1:
       a = x - 1;
       return a * a;
      

      The scope of a variable is the set of lines where a variable is accessible – it is fixed at compile time. The cleanup function is called based only on when the code flow leaves the scope of the variable – it simply doesn’t care about what happened to the variable before that. Also, note that the cleanup function is not called if you reassign some other value to the variable.


%d bloggers like this: