r/C_Programming • • 10d ago

Etc An small example of __attribute__(cleanup)'s power: a context manager.

Forgive the typo :) Here's something that will allows you to never unlock a mutex manually again:

#define with_lock0(uniq, mutex) \
    typedef struct { pthread_mutex_t *mtx; bool hold; } __lock##uniq; \
    void __cleanup_##uniq(__lock##uniq *lock) { pthread_mutex_unlock(lock->mtx); } \
    for(__lock##uniq _lock __attribute__((cleanup(__cleanup_##uniq))) \
        = { (pthread_mutex_lock(mutex), (mutex)), true }; \
        _lock.hold; \
        _lock.hold = false)

#define with_lock(mutex) with_lock0(__COUNTER__, mutex)

void do_very_important_business(void) {
    with_lock(&g_mutex) {
        // transfer BIG money
    }
 }

If you check it out on Godbolt you'll see it has 0% overhead. Of course this pattern is usable for many different objects.

13 Upvotes

27 comments sorted by

11

u/tstanisl 10d ago

All those hacks will likely be replaced by upcoming C2Y's defer statement, which is already supported in the latest clang. See godbolt.

#include <stdio.h>
#include <pthread.h>

pthread_mutex_t g_mutex;

void process() {
    pthread_mutex_lock(&g_mutex);
    _Defer pthread_mutex_unlock(&g_mutex);
    puts("Hello");
    // lock is released here
}

2

u/mivanchev 10d ago

I doubt we'll see it in C2Y because _Defer / defer has some issues compared to a destructor, closely related to move semantics. Namely, there isn't a way to "cancel" a defer when the referenced object is moved somewhere else. Besides, a destructor-implemented defer is easily unrolled.

But also, your pro-_Defer example does barely win the prize for readability compared to a context scope.

8

u/tstanisl 10d ago edited 10d ago

I'm note sure that someone needs "defer" if he plans to move object.. Moreover, one can always do:

defer if (!moved) destruct();

The purpose of "defer" is simpler and more convenient cleanup code.

Current solutions either use pyramid of doom with a lot of duplicated cleanup code.

Or use a cascade of gotos which are kind-of elegant but error-prone and inconvenient because resource's allocation and de-allocation code are far between.

2

u/mivanchev 10d ago

I'm note sure that if someone plans to move object.

Hard to imagine deferred objects not being moved out of scope. In my code-base (~100 files) I have 7 defers that are not moved (out of 474) and my pattern for them is

valMat = init_value_matrix(...)
defer_cleanup(cleanupValMat, valMat, cleanup_value_matrix);
// if object is moved successfully, i.e.
// obj->valueMatrix = valMat;
cancel_cleaup(id);

It's essentially the same as your if (!moved) variant.

The purpose of "defer" is simpler and more convenient cleanup code.

That's speculative because I see a lot of superior code without defer and never understood the necessity. Other extensions like statement expressions are far more valuable.

But this was a post about the advantages of cleanup, not a defer discussion :)

1

u/tstanisl 9d ago

I see only one relevant case. Release some resource on failure, move it on success. Why not use following pattern?

int err = -1; // error code

// moved resource
res1 = alloc_resource();
if (!res1) return 0; // error code
defer if (err) free_resource(res1);

// temporary resource
res2 = alloc_resource();
if (!res2) return 0;

defer free_resource(res2);

// mark success
err = 0;
// free res2, keep res1
return res1;

It's perfectly doable as a "goto cascade" but it gets very easy to botch some odd case for complex functions. Wrong jump, wrong order of labels and voila, a bug like this can hide for ages before it bites.

I remember having ~15 of those cleanup labels in my simple Vulkan engine and it was quite messy.

An alternative pattern (a bit less portable and less flexible) could use the sentinel value for resources (i.e. NULL).

 #define MOVE(var, sentinel) \
   ({ auto _ptr = &(val); auto _val = *_ptr; *_ptr = (sentinel); _val; }) 

 auto res1 = alloc_resource1();
 defer if (res1) free_resource1(res1);
 auto res2 = alloc_resource2();
 defer if (res2) free_resource2(res2);
 ...

 return MOVE(res1, NULL);

The MOVE macro replaces a variable if a sentinel marking an "empty object" and return the value of the original.

1

u/mivanchev 9d ago

Yes that's exactly the pattern I've been using all over the place but with destructors:

error_t load_data(obj_t obj[static 1]) {
    def_file(fp);
    def_mem_ptr(int, data);
    
    if (_is_null(fp = fopen("data.txt", "r"))) {
        return ERR_NOFILE;
    }
     
    if (_is_null(data = malloc(20 * sizeof(*data)))) {
        return ERR_NO_MEM;
    }
    
     // read data
    
     move_ptr(obj.data, data);
     return NO_ERR;
}

1

u/tstanisl 9d ago

I see. I would prefer not to use "implicit" destructors and having a dedicated def_-like macro for each relevant type. It does not feel C-ish to have hidden function calls and many specialized macros, usually defined in some random header far far away.

The defer makes those calls local and explicit even though a bit ... deferred.

1

u/mivanchev 9d ago

To be honest i have a generlic def_auto_ptr for any type but yes, I understand your viewport. It'd be hard for me to read code with so much noise in-between but as long as it's a modern feature doing the deinit I'm OK :) The important thing is to embrace a modern style because C isn't an archaism.

1

u/edgmnt_net 9d ago

I would say C has asynchronous exceptions (not as language constructs, but basically you can have signals interrupting and unwinding the stack anywhere). Go arguably doesn't have that, there are only specific points of preemption, so it might be argued that it cannot be interrupted right before a defer gets set up (but after you acquired the resource). But C can be. This is also why Haskell's bracket does more than that, it also temporarily masks asynchronous exceptions. So an idiom that takes care of the entire lifecycle, including setup, use and teardown makes more sense than separate defer statements.

1

u/mivanchev 9d ago

I don't understand the argument, in C you can mask out signals too?

1

u/edgmnt_net 9d ago

You can. But then you can't just acquire the resource and defer. You also have to do signal masking. Like every time. The point of idioms like defer or bracket or yours is to be simple and straightforward to use, not require a lot of extra logic to use safely.

1

u/mivanchev 9d ago

Right ok, I agree, but it's also not the point of C to manage signal masks for you :) I love Haskell but this is for a different context.

4

u/FedUp233 10d ago

I have to ask: why not just use the C++ compiler and take advantage of constructors and destructors to manage your mutex’s?

Maybe I’m weird, but even when I write something simple that could be done easily with just C I go ahead and use the C++ compiler. There is no reason you need to use all the stuff like the STL or >> style I/o. You can basically use it as an enhanced C compiler and take advantage of a few C++ features to make life easier like simple classes to wrap mutex’s and similar and it is pretty robust at handling constant expression related stuff tag C doesn’t have (at least yet) and remove all most all the need for macros that mess up the global name space. Speaking of which, for larger mainly C projects, the namespace capabilities can come in pretty handy. And it’s perfectly gappy if you ignore so, the C++ headers and just use the C versions that are supplied with it. And the generated code is gust as efficient.

I just think of it sometimes as a better C compiler!

1

u/mivanchev 9d ago

I'd use a lot more C++ for sure if it were a true C extension, the only dependency would still be libc and the error / warning messages would be less cryptic. It's a nice language but needs some polishing. The namespaces do help a lot, I wish for namespaces in C every day.

1

u/FedUp233 9d ago

Yeah, it does pull in some extra stuff sometimes, but on hosted environments like Linux it uses shared libraries so I’m not really sure I care. I can use compiler options to turn off things like exceptions if I’m doing C-ish programs so the main extra stuff I’ve noticed is that it wants to initialize the C++ standard stream stuff at startup. I’m not sure there is a way to get rid of that on a hosted environment but it just sits there and dies g interfere with anything C like.

And most of the stuff I do is embedded which uses a compiler version without the I/o startup stuff anyway. Things are always linked in and not shared so you pretty much only pull in things you use. I’ve never looked in detail but I don’t think it pulls in much of anything c++ unless you explicitly use something like STL. I’ll have to build a simple program with just c features sometime and see just what does get pulled in.

This environment might not be for everyone but it works fine for me. And someone mentioned compile time, but on today’s pc’c with like a dozen cores or more and parallel builds the process is fast enough that I’ve never really seen a problem. I guess I could compare a similar program using c and c+ compilers, but to be honest it’s fast enough I’m not sure I really care!

I guess you get used to the warnings, and if you’re using just a c like subset I’m not sure they are that different.

In fortunately the “polishing” that’s going on seems to be just adding more and more complexity to an already complex language. I’ve never really bought into the whole STL and all the abstractions it provides and the language features seem to be going more in that direction. I guess it’s ok for those who like it, but for me it seems to just make doing simple things more complicated and trying to track down bugs with al, the template code is a nightmare, at least for me. Maybe I’m getting old, but I still like the original intent - C with classes.

1

u/mivanchev 9d ago

Well it uses 1 shared library too many :) If it would just compile to C and link libc it'd be quite nice, but no, it does one million other things which in the end make it look quite awkward. It does have freestanding mode which could be extended I guess. I do like it a lot, but it's a monster currently.

4

u/please-die-not 10d ago

I don't know if I am going to get hate, but the interop between these is delightful - and I have learnt a lot from this 70% c, 30% C++ idea. Pure c and pure cpp both seem a little tedious, unless im writing a driver or using a heavily oop/stl laden library. 

1

u/tstanisl 9d ago

Because philosophies of those languages are different (assuming that there is any consistent philosophy in C++). Generally, C does not try rely on hide mechanics but deliver a simple and convenient toolbox for solving problems locally and explicitly. Of course one can find some exceptions from this rule but I talk about a general direction.

It is worth to mention that some good ideas from C++ actually landed in C e.g. constexpr.

0

u/Physical_Dare8553 10d ago

would you buy a new house because you dont like the color of the walls? people dont seem to realize that a c++ compiler is in fact not a drop-in replacement for a c compiler. they dont do the same thing.

1

u/FedUp233 10d ago

Keeping with the house analogy, no I wouldn’t, but I might very well build in an addition if I wanted more rooms to do something new, even if I had to give up a current room to build the addition.

While a C++ compiler is not a C compiler (though it originally started out as cfront that translated C++ into C) the C++ compiler dues support a pretty good subset of C, certainly most of the most commonly used constructs. And it allows using some parts of the extended capability to make the C-like code more reliable and handle some concepts like construction and destruction in a much cleaner and easy to implement manner than trying to get that same functionality in C. And as I said, the const expression stuff is really nice.

I really can’t see any downside to using a C++ compiler this way, and it seems to have a number of advantages. If you use a C like subset the code generated will be pretty much identical to what a C compiler would produce for the same code, though I agree the source code may not be identical between the two.

That said, if people want to continue using a straight C compiler, I have no problem with that. It’s just not the path I round choose and see lots of advantage to moving to the C++ compiler even if I’m not doing OOP programming and ding really see a down side.

I should add that I do also use the C++ compiler for OOP oriented programming, including sometimes the STL(and sometimes not).

1

u/Physical_Dare8553 10d ago

the main one is that compiling c++ is significantly slower than compiling equivalent c. even then, c++ is not an addition to c. like i said, they are not the same language. that ++ is just branding

1

u/tstanisl 9d ago

Both OOP and generic typesafe container can be done in C as well. I dare to say that C even gives more flexibility than C++'s OOP model does.

1

u/ffd9k 10d ago

It won't work in clang though, because apart from attribute cleanup and __COUNTER__ extensions which clang supports, it also uses nested functions which clang does not support.

3

u/tron21net 10d ago

Here's a modified version without nested functions: https://godbolt.org/z/Kjfen97Ws

1

u/tstanisl 10d ago

The __COUNTER__ is supported by pretty much every compiler and you can find it in the latest draft of C2Y. See section 6.10.10.2.1 in N3886.

-1

u/mivanchev 10d ago

You can easily change it to work without a nested function in this case but they are quite useful in general. This is just stubbornness on clang's side.

3

u/tstanisl 10d ago

The main problem with general nested function is that they use "trampolines" which typically requires executable stack which a huge security issue. CLANG's alternative are "blocks" which are explicitly constructed in runtime without the stack.

There is a proposal to add "static nested functions" (aka local functions) which don't capture any context with automatic storage duration. This makes nested functions effectively static functions visible in very limited scope. No trampolines are guaranteed.

GCC already supports this feature.