In danger of immediately sounding “pompous”, I will bravely postulate, that most developers just simply do not understand how distant is so-called “Modern C”, from what they usually perceive as C programming language.
Allow me to push you into the deep end of the pool, straight away. Here is one Godbolt sample, I prepared earlier. If you went there and came back confused please proceed. Otherwise, this post is probably not for you. Here is my formula:
Modern C = Standard C + ( GNUC | clang )
- Standard C is C17 as of today, managed by the WG14 committee.
- GNUC is well known (and still) the reference compiler for the C programming language.
- clang is a “C language” compiler of the LLVM suite
So why this “GNUC or clang” part of the formula? Simply because these two compilers have developed a powerful set of C extensions still staying true to the standard as much as possible. Reasons being purely pragmatic. Dramatically improved code resilience, safety, and scalability. And developed by teams understanding it is a waste of time trying to replace C.
We shall use SystemD open source project as an optimal use case. It is a well-known, complex, mature, and brilliant C program. Studying its source will make you understand (sooner or later) what is Modern C. What are the reasons for the existence of (in this case) clang attributes, and how that philosophy makes SystemD first possible and second, software part that is crucial for modern Linux. (hint: yes clang and GNUC attributes are almost the same.)
There are many seemingly equally fit for the task, programing languages in existence; yet somehow modern C is the most feasible one for the task at hand: mission-critical system software. The keyword is: feasible.
I might think in that little sample of mine I have managed to show how are attributes, macros, and the rest of the C paraphernalia used today. Where the key assumption is: you want the fastest and safest possible core code, but still not in assembler, and actually maintainable by someone years or decades after you. As a part of some living system. Hint: no I do not claim that little code is something never seen before. I simply claim it shows the power of modern C.
For now and for the end I will present (hand cleaned) the output of the precompiler from the code behind that link:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
/* this is used as mandatory argument for the: TYPE __attribute__((__cleanup__(CLEANER_FUNCTION))) VAR_NAME = HEAP_ALLOCATED_SIZE where __attribute__ and __cleanup__ are clang and GNUC keywords. the trick: signature of the CLEANER_FUNCTION, should be `TYPE **` but it is not. compiler sees `void *` and obediently passes any pointer as cleanup_free argument inside we cast back to `void **` and then dereference that to `void *` the proper argument for the free(void *) standard heap deallocation function. */ static inline void cleanup_free(void* p) { free(*(void**)p); } /* Here we use: Flexible Array Member (FAM), feature of standard C https://www.geeksforgeeks.org/flexible-array-members-structure-c/ This give us structure that holds array and its size, mandatory to be defined at the moment of creating the instance of this struct. Thus we shall have full array description, of any size, of the type float */ typedef struct floatS_ { size_t size; float data[]; } float_array; /* size is here decided at runtime, not compile time and dynamicaly allocated hint: we could do the same by using the VLA standard C feature but that will exppose us to potential stack space overflow. Note how we return a pointer altough we could return the instance, and that will not do the copy elision of the FAM. Also note the result can be NULL if calloc fails. */ static inline float_array * new_float_array(unsigned size_) { float_array* retval = calloc(1, sizeof(float_array) + size_); if (retval) retval->size = size_; return retval; } /* We already sad FAM is not copied if we copy the struct instances ditto we do it `by hand` */ static inline float_array * copy_float_array(float_array * src_) { // this is assert macro expanded ((src_) ? (void) (0) : __assert_fail ("src_", "/app/example.c", 51, __extension__ __PRETTY_FUNCTION__)); // declare, define and allocate the pointer to be returned // notice how we calculate the size of the instance to be copied float_array* retval = calloc(1, sizeof(float_array) + src_->size); // copy the lot, including the FAM, to the result, if calloc has not failed if (retval) { memcpy(retval, src_, sizeof(float_array) + src_->size); } return retval; }; /* same as above but for the array of chars */ typedef struct charS_ { size_t size; char data[]; } char_array; static inline char_array * new_char_array(unsigned size_) { char_array* retval = calloc(1, sizeof(char_array) + size_); if (retval) retval->size = size_; return retval; } /* In standard C we can copy struct instance to the struct instance, but alas if FAM is inside that will not be copied, thus we shall do that `by hand` */ static inline char_array * copy_char_array(char_array * src_) { ((src_) ? (void) (0) : __assert_fail ("src_", "/app/example.c", 56, __extension__ __PRETTY_FUNCTION__)); char_array* retval = calloc(1, sizeof(char_array) + src_->size); if (retval) memcpy(retval, src_, sizeof(char_array) + src_->size); return retval; }; int main(void) { float_array* __attribute__((__cleanup__(cleanup_free))) f255 = new_float_array(0xFF); f255->data[0] = 3.14; float_array* __attribute__((__cleanup__(cleanup_free))) f255copy = copy_float_array(f255); ((f255->data[0] == f255copy->data[0]) ? (void) (0) : __assert_fail ("f255->data[0] == f255copy->data[0]", "/app/example.c", 63, __extension__ __PRETTY_FUNCTION__)); char_array* __attribute__((__cleanup__(cleanup_free))) c255 = new_char_array(0xFF); c255->data[0] = '!'; char_array* __attribute__((__cleanup__(cleanup_free))) c255copy = copy_char_array(c255); ((c255->data[0] == c255copy->data[0]) ? (void) (0) : __assert_fail ("c255->data[0] == c255copy->data[0]", "/app/example.c", 68, __extension__ __PRETTY_FUNCTION__)); return 42; } |
Quite interesting isn’t it? (hint: here is that “auto-free” discussion)
As “we go along”, I will be quite happy to explain and answer all your questions.