1. 15

The gcc extension __attribute__((cleanup(...))) is supported by clang as well, meaning at least in the Linux world it’s a viable way to manage memory and other resources. Yet no one talks about it and the Linux people are going so far as to try Rust for memory safety. My question: why not try __attribute__((cleanup(...))) first? Eg

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

#define defer(F) __attribute__((cleanup(F)))

void freep(char** p) {
  if (*p == NULL) return;

  free(*p);
  *p = NULL;
}

int main() {
  char* msg defer(freep) = malloc(16);
}
  1.  

    1. 25

      Is it not well known? I’ve used it and seen it used for about 20 years. I mostly use it for lexically scoped locking. It’s also essential in any codebase that invokes foreign callbacks because it’s the only way of emitting exception cleanup code in C (on *NIX. SEH extensions work on Windows). If you hold a lock and invoke a callback, which throws an exception through your code, you need to be able to release the lock, which you can do with this attribute. I tend to always use lock-for-scope macros for C locking.

      I suspect part of the problem is that, after using this attribute for a couple of years, you realise that RAII is actually very nice and having language support for it, along with real destructors, is better than macro hacks, and making your C code compile as C++ is fairly easy. So then you just use C++, and then the cleanup works properly if you compile with Visual Studio or other non-GNU-flavoured compilers as well. Having to remember to stick the cleanup attribute on everything is far more annoying than just having a destructor that works and does the right thing by default.

      Also, the first line of freep is redundant. Calling free with null is well defined and so you’re doing this check twice on your fast path, which increases code size and (marginally) hurts performance.

      1. 21

        Automatic cleanup is nice (I wasn’t aware of this builtin, not that I work in gcc much), but there’s a lot more to memory safety. Indeed Rust safety guarantees/assumptions don’t even include not leaking memory.

        Edit: Apparently they did discuss using cleanup in kernel: https://lwn.net/Articles/934679/

        1. 19

          I think it is well-known and little-used. It is a non-standard attribute, it’s not part of C language. If you are willing to go as far as extending the language, you might as well switch to “C with destructors” by compiling your code as C++.

          Folks who don’t switch to C++ generally care about portability across compilers a lot, and for them non-standard extensions are a big no.

          A more specific question is why this isn’t being used by the Linux kernel? The answer is that it is being used:

          https://github.com/torvalds/linux/blob/v6.12/include/linux/cleanup.h

          1. 7

            you might as well switch to “C with destructors” by compiling your code as C++.

            Nothing wrong with your comment but every single time I think “yeah I’ll just write this in C++ and get destructors for free” I come back crying about the rule of five. It sucks there’s so much boilerplate needed to add a destructor.

            Which instead results in me guiding myself to code that does not use destructors at all, because boy I do not want to type out a copy constructor, copy assignment operator, move constructor, and move assignment operator every single time…

            1. 14

              99 times out of 100 you need rule of zero. Implementing user-defined destructors is an exceptional case. In rust-analyzer, there’s 45 impl Drop for 400k lines of code (and like a third are tests for analyzing impl Drop). I would expect the ratio of cases that need an explicit destructor in C++ to be similar. It probably will be significantly larger in the kernel, but significantly larger than once per 10k lines is still pretty rare.

              The value here is not that you can implement a custom destructor, the value (questioned by some) is that compiler automatically generates all the code to use your custom destructor. There are by far more usages than definitons of true resource types.

              Though, it is (part of) a social problem with “just compile as C++” that you need to educate people to not add needless destructors and instead lean onto existing RAII containers.

              1. 10

                The rule of five / three is not requiring boilerplate, it’s requiring you to be explicit if you want to move away from the default. The default destructor on an object calls the default destructor on all of its members. If you are using types such as std::vector or std::unique_ptr that have destructors, they will be called and you don’t need to write your own.

                If you do write your own, it means that you’re doing something unusual with ownership. In this case, you need to design what happens when you copy or move the object. Or, conversely (it’s usually this way around), if you create a custom definition of what copy or move means, you probably also need to define what cleanup means.

                If you are writing something that holds raw pointers or some other resource (for example, file descriptors) then you need to specify what happens if someone moves or copies it. The move case is particularly important with respect to destruction: you need to handle being destroyed if you no longer own the resource that you are encapsulating.

                Normally, the correct rule is zero: do not implement any of these. You use them in foundational things such as smart pointers or owning wrappers for other types, everything else just composes those.

                1. 5

                  You only need those if you might copy or move instances (which would require at least as much code to implement in C too!) If you aren’t going to do that, just declare a copy constructor with “=delete” and you’re good to go.

              2. 5

                My experience with using (nonstandard) scope_guard in a fairly large C++ project suggests that ad-hoc resource cleanup using a defer() equivalent (in any language) is more error-prone and difficult to maintain than compositional RAII (i.e., you don’t have to write any destructors except in the leaf resource management classes).

                1. 4

                  It’s quite a few more lines that could be instead handled with a kernel-style goto before returning. Separating the cleanup into a different function also makes the extra cleanup non obvious. (Will it be the same everywhere? How much context will need to be provided?) And finally you’ll need two different ways to cleanup - one for locals and another for returned values, because they can’t use cleanup().

                  The ability is interesting, but I avoid it because I don’t think it actually improves the code overall. And as the other commenter said - this doesn’t provide memory safety. It’s just a small part of a GC with trivial destructors.

                  1. 4

                    i use that extension, but it doesn’t make C memory safe. there’s nothing stopping you from forgetting to use it

                    also, you have to remember to transfer ownership. (in this case, by nulling any pointers you “move” elsewhere)

                    and there’s no reason to use malloc if you’re going to free it at the end of the function

                    1. 3

                      My question: why not try attribute((cleanup(…))) first?

                      Both linux and systemd are using __attribute__((cleanup(...))). It’s non standard, it’s still rather unusual to read and it mostly just helps with different return paths. It’s also rather convoluted to return a pointer from a function that is marked with the cleanup attribute. Whereas a macro over __attribute__((cleanup(...))) looks mostly the same everywhere, the “return without cleanup” part is entirely unknown to most tools. In the linux kernel you need to replace return t with return_ptr(t) which is just not particularly nice.

                      I’m highly skeptical of that attribute unless it becomes a standard and I dislike reading code that uses it.

                      1. 3

                        My problem is that it’s not super useful. Often, there is more to “cleaning up” something than an operation you can do on just one pointer; for example, it might come from some object pool or custom allocator or some other system, so that “freeing the object” means asking the owning system to free the system. __attribute__((cleanup)) really only works if the object is allocated by some process- or system-global singleton such as the memory allocator or filesystem. This is a problem in RAII-languages like C++ and Rust as well, but in those languages, objects typically contain a reference to their owner such that the object itself knows how to ask its owner to destruct itself (or the object is wrapped in some reference type which knows both the reference and the object).

                        That, and it’s a GCC extension, as you say. I try to write C, not GNU’s custom dialect of it.

                        Also, it completely fails to account for the use-case where you want to allocate an object which should get freed if the function fails but not if it succeeds; e.g a factory function which allocates an object, does some stuff to initialize it, frees the object and returns NULL if that initialization fails, or otherwise returns the initialized version of the object. C++ and Rust solves this problem with moves, languages like Zig solves it with errdefer (so you can free an object on return only if the function fails).

                        So I know of it, but it doesn’t excite me like true language-level ‘defer’ or RAII would. I’m probably not alone.

                        1. 2

                          In C++, scope_guard::dismiss() solves the “elide destructor on success” problem, but you have to remember to call it in exactly the right place. I think errdefer() is much nicer.

                        2. 2

                          It doesn’t seem to be supported in MSVC.

                          https://developercommunity.visualstudio.com/t/add-support-for-gcc-like-cleanup-attribute-for-pla/1187001

                          Maybe it’s possible to emulate it using __finally.

                          1. 2

                            I use it. It makes early returns less scary in C which is quite nice. I do hope defer makes it into the standard.

                            1. 2

                              I think it’s fairly well known, and I think the reason Linux people are “trying rust” has more to do with playing politics, and less to do with memory safety than you might think.

                              But I can give you an idea as to why (at least some) C programmers don’t do this:

                              1. You can almost certainly just char msg[16]; which would be faster/better code.
                              2. If it’s so big you certainly couldn’t, you definitely won’t be better off using malloc.
                              3. But seriously, the stack is big: you probably can.
                              1. 15

                                “trying rust” has more to do with playing politics

                                Linus has given reasons beyond the strictly technical, like attracting new talent, but reducing that “playing politics” is an inflammatory framing.

                                On technical level, Rust’s solution is much more comprehensive, handling more than simple callback per scope, and more language integration prevents more footguns.

                                Unique ownership + move + destructors ensure that:

                                • Newly created values are dropped up automatically, and don’t need cleanup set each time. The compiler tracks when variables are (un)initialized, and won’t try to run cleanup on uninit data.

                                • Drop on error only, and returning the allocated value on success doesn’t need extra code to disable the cleanup. With the cleanup attr if you forget about this you get UAF. In Rust you can’t forget.

                                • It also works reliably in more cases like loops and collections, and handles cases when values are inserted or moved out of the collection or passed to another function. The scope-based cleanup call needs to be defused when the value is moved, and won’t trigger on the old value when a variable is overwritten with a new value.

                                • Borrow checker ensures there won’t be any other pointers left pointing to the cleaned up object. For example Rust uses this to ensure you can’t keep using a pointer to an object protected by a lock after releasing the lock.

                                1. 0

                                  Linus has given reasons beyond the strictly technical

                                  Perhaps, but outside of the technical, everything else is politics, and if you don’t think that’s a bigger part of what rust is doing there than the former, I’d love to hear why, ideally with quotes from Linux kernel developers talking about features they’re looking forward to being able to use when rust is more prevalent in the Kernel.

                                  On a technical level…

                                  As far as I know that’s all true, but none of it is useful or relevant to what we are talking about which is why C programmers don’t seem to use attribute cleanup.

                                  1. 6

                                    Even if thinking about the future of the project gets labelled as politics, describing things as “playing politics” is still a dismissive phrase.

                                    none of it is useful or relevant to what we are talking about

                                    I’m not answering why cleanup isn’t more known, but telling you why developers are “trying rust” instead of staying with incomplete unsafe stopgaps like the cleanup.

                                    1. 0

                                      Even if thinking about the future of the project gets labelled as politics, describing things as “playing politics” is still a dismissive phrase.

                                      What do you want me to do about that?

                                      telling you why developers are “trying rust” instead of staying with incomplete unsafe stopgaps like the cleanup.

                                      Why do you want to tell me that? It’s not related to what yawaramin is saying… or what I was saying to yawaramin… I don’t think anyone is wondering why random developers are trying rust, but I think yawaramin rates the kernel developers as capable, and wonders why they don’t use “unsafe stopgaps like the cleanup” in the first place, but seem willing to try rust. And yeah, I don’t really think they’re as willing as yawaramin thinks, that’s all.

                                    2. 1

                                      As far as I know that’s all true, but none of it is useful or relevant to what we are talking about which is why C programmers don’t seem to use attribute cleanup.

                                      That’s not what we’re talking about, though. We’re talking about your claim that Linux adopting Rust is mostly about “politics”. You can’t make a claim and then deflect by saying that counter-claims are irrelevant and not topical. That’s just trolling.

                                      1. -1

                                        we are talking about which is why C programmers don’t seem to use attribute cleanup.

                                        That’s not what we’re talking about, though.

                                        That’s not what you want to talk about, that’s clear, but since you replied to me and not the other way around, you need to understand that’s actually what I’m talking about.

                                        The author theorised that Linux developers didn’t know about attribute cleanup, and conjectured that this explained why they were looking hard at rust. I disagree with both the premise and the conclusion and gave my reason why in a way that has clearly incensed rust developers.

                                        We’re talking about your claim that Linux adopting Rust is mostly about “politics”.

                                        I don’t really care how important Rust programmers think Rust is, but I’d be very interested in what gets an experienced C programmer excited about seeing Rust in the Linux kernel. Oh here’s one here:

                                        https://www.theregister.com/2024/09/19/torvalds_talks_rust_in_linux/

                                        and the Horse Himself says it’s “livened discussions”, which is a very political way of saying nothing. I’m not sure I believe Linus thinks rust is actually important for ‘attracting talent’, but I’m sure that’s a political goal too.

                                        You can’t make a claim and then deflect by saying that counter-claims are irrelevant and not topical. That’s just trolling.

                                        I am under no obligation to address or even acknowledge baseless irrelevant claims that anyone is making themselves.

                                  2. 2

                                    And if the stack is too small simply increase the stack size. I prefer a huge stack to allocations, young me had some bad experiences with memory fragmentation on consoles and early phones.

                                  3. 1

                                    Wondering what would happen if after adding that to the entire kernel source tree, something wrong happens (say, edge case problems due to some freshly discovered cpu hardware bug) requiring a cleanup of the whole attribute(cleanup) thing…

                                    1. 1

                                      It cleans up upon function return, which is a bit of a niche use case.

                                      More common cases are to automatically cleanup only when function foo() has failed and errored-out so what one does not have to stackup cleanup labels; or to cleanup some time later from a different function bar().

                                      One might think this can be useful to automatically release all the locks that function has acquired, but even this is a little limited, because it’s not entirely uncommon that one acquires lock in foo() and releases in bar().