Giving C a Superpower: custom header file (safe_c.h)
24 points by linkdd
24 points by linkdd
Perhaps this is a failure of imagination, but it read to me like a corollary to Greenspun's 10th Rule. It'd be easier and significantly less error-prone to use a C++ compiler and standard library, and ban whatever C++ features you don't like by convention.
I think the usual counterargument is that, especially as the number of software developers on a project increases, it becomes more likely for more complex C++-isms to sneak into the code, and harder to maintain discipline.
That being said, I don't think what the author proposes is a good idea. If you want a language with a different set of tradeoffs, then use a different language.
I can't argue with the results. If it works for you and you are happy with the resulting code, I can see this being a significant improvement over "normal" c.
Personally, I've tried RAII in C with cleanup attributes and just found it too clunky. Maybe with C2Y's defer proposal this kind of programming might become viable for me. In the meantime, I highly recommend giving custom userspace allocators a shot. Having a way to free all of the memory at once really simplified the way I program.
https://thephd.dev/c2y-the-defer-technical-specification-its-time-go-go-go
https://www.rfleury.com/p/untangling-lifetimes-the-arena-allocator
The idea behind safe_c.h is good, but it's a matter of taste if you want to use that many macros, though. You end up with a metalanguage that is very hard to read for someone not familiar with the codebase. The article inspired me to look further into RAII mechanics in C23.
You considering C23 for your project? I'm still firmly in C99 with some clang extensions, but comments like yours makes me think C23 is getting ready for prime-time, so I might soften my stance soon.
#embed, bool, max_align_t & alignas & alignof, atomics and thread local storage, static assertions, [[attr]] syntax, nullptr & nullptr_t to name only a few features introduced in C11, C17 and C23.
The question really is, why would you stick to C99?
For my C coding, I don't use threads, so I don't need those nor atomics. I see why nullptr was added, but I hate it (couldn't it have been NULLPTR to make it stand out better?) #embed is nice, but I rarely need that, and C99 already has bool (through a header file). Besides, there are people out there stuck using C89!
Right, on principle I err on the side of longevity, such that my projects are maximally portable (read: may be compiled even by very old compilers.)
What would tip the scale is mass adoption of later C standards AND all the newer features are far too convenient to ignore.
Honestly I don't like this approach. It uses macros too much and tries to add RAII to C. Personally I think the saner approach would be to create a sort of stdlib on top that you use that does bounds checking and whatnot and then use that. For the allocation problem you'd allow using different types of allocators for different strategies. You can usually get quite far by just switching between arenas, temporary allocators and the odd standalone allocation from ur general purpose allocator. You just have to be aware of the lifetime of ur objects.
I think macros are probably one of the most interesting feature of C, and as you point out using them will raise some eyebrows, but this quite a creative, and dare I say, elegant use of them. More importantly, it brings back good old C to today's concerns of memory safety and modern ergonomics. It's in old pots that you make the best soups, as we say in French.
I feel like the approaches described in this post only buy into the techniques on a superficial level. For example, to get an automatically unlocking mutex, they propose:
pthread_mutex_t my_lock; // A
pthread_mutex_t* lock_ptr CLEANUP(mutex_unlock_cleanup) = &my_lock; // B
pthread_mutex_lock(lock_ptr);
if (some_error) {
return; // Mutex is automatically unlocked here!
}
What I dislike about this is that you now need to write two lines to get an automatically unlocking lock, both A and B. If you forget B, I'm assuming the cleanup doesn't happen. That feels like a footgun to me (though admittedly one you could easily write a linter for).
I admit that, lacking proper C experience, I might not be able to see that this is a huge improvement over what the default C case looks like. However, it just feels like a mostly syntactic improvement, a thing you would do to shut up your direct manager, without actually having to rewrite your codebase to C++/Rust.
My impression is that people who really believe in RAII, would only use proper C++/Rust style destructors that don't rely on the user writing code properly and the compiler supporting a possibly non-portable extension. Meaning that this proposal is insufficient. Then again, maybe I'm being too skeptical, and this small syntactic reduction of the problem is just the thing needed.
For what it's worth, I have use something like this in C projects with some success, but I don't use the raw form. For example, in the Objective-C runtime, there's a LOCK_RUNTIME_FOR_SCOPE macro, which acquires the global lock and holds it until the current scope ends (close brace, return, break, or exception). The last of these is nice because, although the C code in the runtime doesn't ever throw, other code that it invokes may try to unwind through it and this makes it possible to write exception-safe code in C. There are a few other similar things.
For new code in that project, I've largely moved to C++, because I had a big pile of macros to implement a hard-to-use and hard-to-debug subset of C++ in C. And, at some point, you realise that this is a waste of everyone's time and just use a language that has these built in from the start. I used to have a horribly complicated generic hash table implemented in macros, and replacing this with tsl::robin_map made the code simpler, fixed a load of bugs, and made it perform better. A language that doesn't have type-safe generic data structures is not suitable for systems programming.