Some obscure C features
mort.coffee
mort.coffee
struct Element {
int value;
struct Element *next;
}
#define ELEMENT(value) _ELEMENT(value, __COUNTER__)
#define _ELEMENT(value, n) \
struct Element CAT(e, SUCC(n)); \
struct Element CAT(e, n) = { (value), &CAT(e, SUCC(n)) }
You can then walk from e0 through the linked list, but because of how the initialization works, you're done when the current element has `next == 0`. int main(void)
{
struct Element *e = &e0;
while (e->next) {
printf("%d\n", e->value);
e = e->next;
}
}
Here's a repl.it: https://repl.it/repls/DetailedYoungAsianconstablebutterflyThe biggest downside is that (as far as I know) it's impossible to get this approach to coordinate across translation units, so you're forced to define a main in each test file (via the library's header file?). But I doubt that matters too much in practice.
__attribute__((constructor))
https://github.com/kbob/schetoo/blob/master/prim.h#L215
This also works across multiple source files, unlike __COUNTER__.
> Sadly, you can't set the value of an index in an array like that in the top level in C, only inside functions.
This is not exactly true. An assignment is a value, you can do e.g.
int a = b[x] = 0;
(which implies that you can do your array-index assignments at the global level by creating nonsensical/unused variables and initializing them).
The linker's job is to combine all the separate pieces in a "section" (special linker term) and write out one section.
Using a special named section lets you create an array using macros, though using it for function pointers is probably safe and using it for other data types might be pretty fragile. http://www.compsoc.man.ac.uk/~moz/kernelnewbies/documents/in...
Turned out the macros I overlooked at the end of each file placed the struct defining the command in a special linker section, and the parser init would go and load all the commands from that section.
Interesting to know that this trick already existed in the Linux kernel.
I'm (perhaps perversely) disappointed we aren't using more of the linker's power in many C codebases.
A primary problem with linker magic is that it is usually very non-portable. Hacks you do with macros may be ugly but at least they work everywhere, unless you used a non-standard behavior.
Linker tricks tend to be platform specific, and even more so that other tricks (eg POSIX platforms share a lot of common behavior, but still tend to have quite different linkers).
I overrided malloc() and free() with my own memory tracking functions.
On Windows side it wasn't needed, as VS already provided some APIs for it.
This is the reason why I do not like such tricks. This obviously hurts maintainability and probably portability. Is the tradeoff really worth it? Plain stupid registration is just a single line. KISS.
"Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?" –Brian Kernighan
https://msdn.microsoft.com/en-us/library/bb918180.aspx
Another interesting thing is that it relies on the linker ordering names in alphabetical order.
The initialization order is implementation dependent in module/package initialization blocks.
The only guarantee is that the modules being imported run first than the one being executed.
Of course it still won't be deterministic if you load classes in parallel in multiple threads.
For example
class A { static final int x = B.y; static final int y = 1; }
class B { static final int x = A.y; static final int y = 2; }
will not produce any static initializers and will do what you probably wanted because everything can be turned into a class constant and resolved at compile time. However this class A { static final Integer x = B.y; static final Integer y = 1; }
class B { static final Integer x = A.y; static final Integer y = 2; }
will not work the way you might want because there is no order in which the static initializers can be run that will do everything in the correct order. // COUNTER.h
#if !defined(COUNTER)
#define COUNTER 0
#elif COUNTER == 0
#undef COUNTER
#define COUNTER 1
#elif COUNTER == 1
#undef COUNTER
#define COUNTER 2
...
#end if
And then change each invocation of ELEMENT to have a leading #include "COUNTER". #include "COUNTER.h"
ELEMENT(x)
#include "COUNTER.h"
ELEMENT(y)
Note that if you take this approach then (as far as I know) you're actually writing standard C!These days I mostly use C, and occasionally resort to python as an accompanying scripting language to generate data definitions (either in plain text formats or as C structures).
Like kernel drivers, embedded (microcontroller) development.
Or perhaps you just need to develop a small loadable library without any runtime (no malloc, etc.). Or perhaps writing Python, Lua etc. C-module for better performance.
You just need to treat C with proper respect, not be too arrogant about your skills.
I hope over time we can move more and more to Rust or some other language which gives as many errors at compile time as possible and helps with not shooting at our feet.
Shouldn't have too many C-macros, of course. Some macro messes I've seen can be nearly impossible to debug...
We've been hearing "C isn't the problem, bad programmers are the problem" for 40 years. This has continued to perpetuate the problems with the language.
I mean, I certainly agree that there are no alternatives to C now for certain domains, but there are real problems with it that aren't solved by just "treating it with proper respect".
But often you just need to get the job done, and have limited options available.
Perhaps in 50-100 years, people looking back at software from this era won't be able to understand how we managed to make it work at all.
I haven't met or know anyone mastering C/C++ (especially C++), and doubt I'll ever hear about one.
C can definitively be mastered. C at it's core is pragmatic minimalism. The basic ideas are very simple. If you want to be a language lawyer and know the standard completely (including all historical accidents), it can get complex, but still manageable. But anyways, you shouldn't be a language lawyer. That's not C. Make maintainable programs instead, stay in the middle of the road.
C gets out of the programmer's way. It's not about mastering C, but about mastering programming. And while I agree there's much bad C software out there: I haven't seen one maintainable non-bloated Java project. Here is one master of C that I'm sure you know: Linus Torvalds. Of course there are many, many more. Just remember it's not about C, but programming machines in general.
If that's true, how come there are almost no widely used network facing C programs that didn't have plenty of security vulnerabilities?
(Anything written by Daniel J. Bernstein doesn't count, because there's no way he's a mere mortal. :))
Postfix is also much more widely used than qmail.
Goto, you just can't do without it.
Also not saying that in some cases even C is too much abstraction. But in general, its memory abstractions and its function call abstraction have proved to be very effective helping humans develop in a modular fashion.
Strange, the last time I used it I was still doing Basic in the early 90's, or when I do some Assembly programming.
So yeah, we can do without it, as long as the languages are powerful enough.
Not saying there's no use for backward gotos, just don't remember ever needing them. Maybe they could be useful for some C code generation cases or perhaps for retrying some operation?
What really annoys me are C++ exceptions used for flow control. Surprise gotos. I loved exceptions long ago at first, but over time the feeling has shifted more and more towards hate. Exceptions make code bases so much harder to understand. Especially when combined with inheritance.
There are indeed C compilers that don't support recursion (or function re-entrancy). What does the standard say about stacks?
Recursive function calls shall be permitted, both directly and indirectly through any chain of other functions.
Stacks, I don't know. I only care about the concept (and semantics) of function calls. The call stack is an implementation detail. I wouldn't be surprised if the concept of a call stack was not part of the standard.[1] http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
> For such an object that does not have a variable length array type, its lifetime extends from entry into the block with which it is associated until execution of that block ends in any way. (Entering an enclosed block or calling a function suspends, but does not end, execution of the current block.) If the block is entered recursively, a new instance of the object is created each time. The initial value of the object is indeterminate.
I also haven't seen one large-scale network-facing C project written using reasonable development practices (i.e. not absurdly expensive formal verification) that hasn't had some sort of terrible remote code execution vulnerability. By contrast, Java programs tend to be far less vulnerable to this kind of problem. (The closest thing is probably bugs with attacker controlled java.io.Serializable, which while severe are much less frequent than RCE in C programs.)
> Here is one master of C that I'm sure you know: Linus Torvalds.
I agree that Linus is a master of C. Linux is also an excellent example of the above problems with C.
Use your tools wisely. Architect programs for clear data flow. Validate inputs as soon as possible. Don't do (de)serialization by hand.
By all means use a managed "memory safe" language if you have many data accesses that are not behind a validation gateway. But for large amounts of data or complex software, that's still not an option. C is still the only language in which I can write software of advanced complexity - because it does not get in my way.
Mesa/Cedar at Xerox PARC, Topaz at DEC/Olivetti, Oberon at ETHZ, JavaOS at Sun, Singularity/Midori at Microsoft Research
> Game Engines
Unity is slowly rewriting parts of their C++ pipeline with C#, after their IL2CPP compiler got mature.
Then there are game engines like Xenko being done.
Also being with one foot into games since the 80's, I remember the transitions "Pascal/C are too slow, we stick Assembly", "C++ is too slow, we stick with C", now we are in "JavaScript/C#/Java are too slow, we stick with C++" phase.
This has gotten a lot better with static code analysis (clang-analyzer, Coverity, ...) and whitebox-fuzzing (libfuzzer, afl, ...). Both simply point you to the bugs and don't require a formal spec.
Together with Valgrind, these tools have revolutionized C programming for me.
Pragmatic minimalism as it was for the hardware at the beginning of the 1970s, to be exact.
The C machine model says nothing about vector instructions, instruction re-ordering, NUMA, GPGPU, IO ports, Harvard architecture, multi-core.
Modern compilers are still rather bad at vectorization. What's worse, those optimizations are often unstable. A small change might make code several times slower. Performance regression tests and when that fails, vector intrinsics are a must.
I’d say I had a working knowledge of C++ after about a year ramping up on a codebase based on a mature C++ framework.
That was after years of writing reliable, performant C code. My C++ is now significantly better than the C I can produce, on both fronts, and at the same time.
I’m also much more productive as a side effect of that.
template<template<class>, template<class, template<uint>>, class>
before breakfast.There are equally important reasons why C's usage has been in steady decline from the '90s to today.
More important was it was the only language that really worked on DOS, which ruled the computer biz for 10 years. I would guess 80% of programming was done for DOS, and C was a very good fit for the 640K systems.
C and C++ had no special place on the spectrum of programming languages usage, unless we were porting code between MS-DOS and UNIX.
Until it was time to move into Windows, and anyone not using C or C++ started to be left behind, manually writing FFI wrappers (e.g. Turbo Pascal/Delphi) and we eventually migrated to one of them.
You always have other options if better languages can compile to C or integrate it via FFI. There's been plenty to do that, too. For simple and safe, Modula-2 was early one compiled to C that couldve had syntax modified to be more like it. If aiming for power, PreScheme was a systems dialect of Scheme that compiled to C at one point. The OcaPIC (Ocaml) project and use of ATS language for 8-bit show stuff like that could probably be made to wofk in areas C is used in, too.
So, I'd say it's a myth you need to develop in C just because a target has only C libraries or compilers that safer languages can use. Instead, we get a new claim that people just didnt do it for reasons that varied per project or person but wasn't technical capability.
How does that sound?
The majority of them also enjoy Pascal and Basic compilers.
So sometimes, there is indeed an option.
Also many languages have compilers that are able to generate C code.
As to ensuring, well that is what testing is for.
Use Common Lisp to generate C code. There are three libs that are specific for this, see C-mera for example.
Is this a joke, or do they do some sort of stack-bounding static analysis?
This is true. With all the new and fancy languages around, it's strange that there are so few attempts at making a better C.
I would say that the characteristics of C is: dead simple, manual memory management, "fancy assembly".
Rust is close, but fails on being dead simple.
The best attempt I know of, is Zig: http://ziglang.org/
But it's still very early in development. I am using it for microcontroller code right now though, and it's much nicer than C already, despite having to deal with bugs.
My guess is that in most (not all) cases C++ is usable instead of C, but the culture of such development is to prefer C. It's a real pity, because for someone with a moderate amount of judgement (i.e. not going off the deep end on unnecessarily complicated C++), it only takes a moderate amount of C++ knowledge to be able to write more maintainable and correct code that performs equally well.
Python has incredible productivity and versatility with ease of learning. It's readable. It's easier to prototype things like verification tools in it than C. There's also tools like Hypothesis to test that. Then, there's tools to speed up the code like Cython.
If anything, Python would be a safer, faster way to write tooling for modifying or testing C code than C itself. One could also convert Python generated tests to a C library of tests with automatic validation of data types, coverage, etc. So, I think it would make sense if someone did anything from prototyping C in Python to using it to test C code.
And do note Ive enjoyed reading your experimentation with macros in C. I learned about defer through it. Just commenting on that one point.
TLDR: because you need to test the real thing, and using Python to test C code causes too many important differences between tests and production.
To test C code you need to compile it, and there’s no C compiler inside Python. Also if you have C++, sometimes you can abuse templates for similar effects, and Python doesn’t have C++ AST either.
C toolchains largely depend on environment: included & linked files, environment variables, compiler & linker switches, they all make a huge difference. You’ll spend much time replicating that in Python, and it’ll break when you’ll upgrade any part of toolchain (compiler/IDE/build automation/etc).
To a lesser extent, same applies to native binaries as well. If you’ll manage to use Python to produce tests, C compiler to build them, then Python to run them — the runtime environment will be different from the real one.
I think nickpsecurity has introduced two ideas, and you are only countering the more complex one. The ideas were,
* Use python as a preprocessor
* Use python to call into C.
To the first idea: the language you choose to use as your preprocessor. Instead of using C macros, you could have an alternate DSL that you transform into C. Then you compile this. Given what an awkward thing the C preprocessor is, I am surprised that it continues to hold mindshare against options like this. Awk is a better powerful transform tool, and easily compiled for any platform it is not already on.
Line numbers is a complication if you do your own preprocessor. This post on PHP suggests how to deal with this, https://stackoverflow.com/questions/396644/replacements-for-...
To the other point. The complications you raise are real, but you can manage them away by setting your C project up to create a shared-object build. For example, it is trivial to use Racket to write tests against shared object files (once you know Racket). This still doesn't address the runtime issue you raise.
Separate issue. There is a set of debugging C-preprocessor macros with similar intent to the ones posted in OP published in "Learn C the Hard Way".
It’ll become harder to find developers, and longer for new ones to start being productive. Also it’ll become harder to do cross platform development because you’ll have to port, and then support, that custom pre-processing tools/DSL for every platform.
> Use python to call into C
Last time I’ve checked Python can’t import C headers. To call into C, you somehow need to negotiate function prototypes & calling conventions between the two languages. Regardless on how you do it manually or automatically, it’s very expensive to support. People do it when the have to (e.g. to interop Python with C libraries), but for tests, the approach is way too expensive for most project.
> it is trivial to use Racket to write tests against shared object files (once you know Racket)
I don’t know Racket but I doubt it’s trivial. SO libraries export C functions, they use C structures for arguments and return values, and these can be very complex. Pointers graph, function pointers, pointers to pointers, variable length structures, other advanced features are pain to use from any other language (besides languages specifically designed to be backward compatible, like C++, objective C, and maybe D). And if you need to test C++ code it’s even harder, unlike C it doesn’t have standardized ABI, i.e. the ABI changes between compilers and their versions.
Agree regarding C++ ABI also. I don't have much experience with C++. My usual approach is to use C for system calls and bare-minimum surrounds, and then use the wrapping approaches described above to get to a high-level language. This is why the struct issue didn't come to mind for me.
* Use python to call into C."
You nailed it. Also note that Python isn't my recommendation so much as what mort brought up that I'm countering with examples showing even it can help. If it was my choice, I'd pick a better HLL for these goals. Let's keep looking at this a bit, though, where I'll introduce those where they're useful.
PHP was a great example I didn't think of on preprocessor. It versus C's is either the 1st or 2nd most used one out there. On the high end, some "term-rewriting languages" can easily handle jobs like refactoring: TXL, Rascal, OMeta, Ohm. Alan Kay et al in STEPS project did a whole OS with graphics stack and all in a mere tens of thousands of lines of code using such a language. One can, as people do with Prolog and STEPS did, pick a general language that can be extended with meta facilities for DSL's or rewriting where opportunistic. Then, where that doesn't work out, you at least have general-purpose language to fall back on.
(Use Control-F to go to anything that says "STEPS" to find the reports. Work from the bottom up since it's chronological series.)
On your other point, your Racket example is one way it could work. I was also advocating in this thread just using Python itself to build tooling for its own benefits and ecosystem. A lot is already built. Use C for low-level software that strictly needs it if nothing else is available with HLL's like Python for stuff that doesn't need it. The language I've been telling C programmers to check out for scripting-like use is Nim. It looks like Python, has real macros, can call C, and compiles to C. Here's a comparison I just dug up:
https://github.com/nim-lang/Nim/wiki/Nim-for-C-programmers
I also doubt it will be harder to find C developers if tooling is written in HLL's. For one, they just have to use the tooling rather than write it. I doubt most C# developers extend Visual Studio, most Java developers extend NetBeans, and so on. If they do have to learn, using a language like Python or Nim should make low barrier to entry since even folks with no experience pick Python up quickly. A C-like subset of Nim or something similar will be just a lot like C with easier syntax and less stuff to worry about. If anything, productivity will go up over C like shown in about every language study ever done.
I have encouraged people to do C-like languages with safe defaults, cleaner semantics, a REPL, better macros, easy integration of C, and outputting C. That way, one can program in a cleaner language avoiding most headaches and accidents that come with C being designed for 1970's hardware. Aside from Wirth's languages with C syntax, a good picture of what this might look like is Julia language which was femtolisp on the inside. Especially its seemless interoperation with C and Python.
I think this is because writing python code is like writing C++ without templates, but all the integer types declared “auto” and all the pointers void.
The main difference is that the C++ compiler still* does more type checking than python even after you abuse the warning flags enough to get it to stop gently telling you to change professions!)
Reading it reminds me of when I accidentally got my emacs syntax highlighting stuck at “white on white” for a bunch of stuff, but I digress.
I guess I find it to be a completely unproductive, unreadable, and unmaintainable language. It’s OK for throw away prototypes, but I’ve never seen a team throw away a python prototype and rewrite it in another language. I know of python projects that failed and were [repeatedly] rewritten by the same team in python, and I know of python teams that failed and got swapped out for another language, but that doesn’t count. Part of the problem with the python prototyping story is that python wants to own the event loop and asynchrony / thread model, and changing those are often the main reason to swap languages — the next step is always a rewrite in my experience.
Wow. Long rant. One too many ‘assert “” != None’ errors this week. :-)
Regarding the event loop, I don't think that's true; you can embed libpython and call functions from your own loop: https://docs.python.org/3/extending/embedding.html
Its basic operations don't often crash a system or lead to hacks. It also promotes readable, concise code with a lot of FOSS utilities for things like testing (esp see Hypothesis). This both increases development pace and reduces defect according to about every study that's ever pitted a HLL against C. The best one I've seen in terms of apples to apples was this Ada and C comparison:
http://archive.adaic.com/intro/ada-vs-c/cada_art.pdf
Note that Python isn't the main point or even what I brought up, though. mort's point read like you shouldn't develop C-related tooling in high-level languages like Python. I'm countering saying you should do as much work out of C as you can with even Python being beneficial. I give much better examples in my other response here:
For example, to produce generic SIMD-accelerated code over both Intel-specific SIMD operations and the Sleef vector math library I needed for FHT-based JL-transforms and kernel projections, I used template specialization to abstract to operations on physical memory. [0]
The generic methods themselves were gross, but in application, it was extremely simple in downstream code to specify an operation and a container and have the compiler pick the fastest method for the job. I’ve also checked my generated assembly and the smaller functions are always inlined, even though they’re called from static constexpr function pointers.
This is also a great use case for macros, as they saved me huge amounts of typing.
I’m sure many will say C++ is the wrong direction to go for metaprogramming, but it’s worked great for my purposes and lets me get straight to the hardware.
[0]: https://github.com/dnbaker/frp/blob/master/include/frp/vec.h
Macro metaprogramming is still needed in C++. Other powerful languages might lack some nice properties of C. Like easy interfacing with other languages, and a wide availability.
I have been working on Snow, a unit testing library for C.
OP requires a unit testing library for C, so they've written one. What is the alternative solution you propose? The framework itself may appear complex, but the entire end goal here is to provide simple/abstract tools to write tests in a readable/self-documenting manner. The framework itself is the mean, not the end goal.(Incidentally, the dynamic linker hack is precisely how both FreeBSD and Linux perform all their kernel initialization: Individual modules define symbols with a particular naming pattern, and then the kernel linker enumerates those symbols.)
> I wanted to see how close I could come to making a DSL (domain specific language) with its own syntax and features, using only the C preprocessor and more obscure C features and GNU extensions.
Are you just nitpicking the author's description of extensions as "C?"
Perhaps the comment author should receive some benefit of a doubt as to his intentions?
Yes, it doesn't denigrate anyone, but that's a really low bar.
I do give authors the benefit of the doubt when something isn't clear. That's why I couched my message in "I think" and asked a question instead of just asserting facts.
You seem to have misinterpreted me. I wasn't saying Colin was attempting to clarify (I didn't know his intent), I was saying that is his comment was factually clarifying. I didn't have any evidence as to his intent at the time, but I didn't see a reason to assume anything other than he was being helpful.
> Yes, it doesn't denigrate anyone, but that's a really low bar.
That's not even half of the criteria I listed, so it's not really the bar I set at all, is it?
The comment contributed by clarifying the submission name for those that had not yet read the article. It additionally contributed by adding some factual information about some of the referenced features and how they are used in the kernel.
I don't think anything he said takes away from the article, so I don't think he's necessarily missing the point.
> I do give authors the benefit of the doubt when something isn't clear. That's why I couched my message in "I think" and asked a question instead of just asserting facts.
I think you also misunderstood what I was trying to accomplish, how much of my statement was meant to be a condemnation and correction, and how much was leveled at you instead of the general readership. The only existing reply to you at the time I posted was referencing people on HN that like to nitpick. It was meant as a "this isn't necessarily negative, so let's just take it for what it provides, and it provides some usefulness" and not as "shame on you for assuming the worst".
Edit: Blah, there was some weird wording in that last paragraph
void (*_test_functions[NTEST_FUNCTIONS])(void);
int _test_function_idx;
#define TEST_FUNCTION(fname) \
void fname(void); \
__attribute__((constructor)) static void _init_ ## fname (void) { \
_test_functions[_test_functions_idx++] = fname; \
} \
void fname()
TEST_FUNCTION(test_foo_bar) {
assert(foo_bar == 0);
}AIX used to implement shared objects just like on Windows, using an export definitions file and import libraries, for example.
Last time I used it was about 2003.
http://maplant.com/jit_bf.html
http://dginasa.blogspot.com/2012/05/jit-compilation-without-...
And __cleanup.
And statement-expressions.
And Blocks is nice too.
All of this should be standardized.
Edit: also, syntax-wise it seems to me that it is better do define the macro such that it can be used as
define_foo(blabla...){ ... code ...}
In this case this would involve code like #define define_foo(name, ...) \
static foo_ ## name(); \
... initializer function ...
static foo_ ## name()
which also solves the first problem mentioned in the article.Personal wish for an extension to c is to be able to create a list function arguments and slop that around until needed.
int BarBaz(int bar, int baz)
{
return bar+baz;
}
auto foo = args_of(BarBaz(10, 20));
...
...
printf( "BarBaz = %i\n", BarBaz(foo)};That might not be obvious from the simplified macros I used in my example, but it's pretty clear by looking at the actual definitions. The describe and subdesc-macros (https://github.com/mortie/snow/blob/a9ad850df456f78bcf96e1aa...) need to increment counters and print their status after the block, and the `it` macro (https://github.com/mortie/snow/blob/a9ad850df456f78bcf96e1aa...) needs to run deferred expressions.
I think it would be possible to implement the `describe` macro such that it can be used as `describe(blah) { ... }`, because that defines a function and we can both give the function argument and expect return values from it, but I can't think of any way to do it with the other macros which just create regular `do { ... } while(0)` blocks.
If I'm wrong, please show me how; the `foo(blah) { ... }` syntax would make the __LINE__ macro work, it would play better with auto indenters and syntax highlighters, and it would give prettier error messages if you have a syntax error in a test case. I just can't see any way it would be possible.
#include <stdio.h>
#define before_after(before, after) \
for(int before_after_##__LINE__ = 0; \
before_after_##__LINE__ < 3; \
before_after_##__LINE__++) \
if(before_after_##__LINE__ == 0) { \
printf("%s\n", before); \
} else if(before_after_##__LINE__ == 2) { \
printf("%s\n", after); \
} else
int
main(void) {
before_after("hello", "world") {
printf("to all the\n");
}
return(0);
}Your snippet won't work with this inline example:
before_after("hello", "world") { before_after("hello", "world") { }}I don't do C for a living any more (barely did) but the number of times I asked smarter people "why did you use this idiom with this terrificly confusing side-effect" and they said "thats not a side effect" comes to mind...
Function building using templates:
https://github.com/faragon/libsrt/blob/master/src/saux/ssort.c
Passing macros to macros (meta-templates): https://github.com/faragon/libsrt/blob/master/src/saux/ssearch.c puts("Line one\nLine two\nLine three");
and puts("Line one\n"
"Line two\n"
"Line three\n");
mean the exact same thing. This can be a godsend for making complex debug/log output more presentable in the source.I wouldn't be so sure John Nagle agrees with such a blanket statement.
Coding in C is already super risky, as proven by the huge number of projects that have exploitable memory errors in there. Adding complexity using features one doesn't really understand is at best going to complicate debugging and make thing work magically and in the worse case, lead to the risk that someone is actually going to understand how your code works and exploit it.
Not worth it.
The "I don't know how that works, but the important part is that it does." sentence was meant to highlight the apparent absurdity of dereferencing a void*, not to say that I don't understand exactly how to use the feature.
$ echo '#define x(a,b,c) a b
x(1, (2, 3), 5)' | cppCalling `describe(foo, ({ int a, b; }))` would make the preprocessor happy, but it would also produce invalid syntax:
void test_foo() ({ int a, b; })Naturally, Clang also supports statement expressions...
EDIT: I highly recommend spending some time reading about all the various GCC C extensions, and what compilers support them (typically Clang, but also Sun Studio, if that's still around..., and maybe others). There are quite a few very useful ones.
Eventually one tires of no support in the symbolic debugger, program analysis tools, compiler error messages that are based on the post-expansion code, nobody else understands the code, hygiene problems, etc.
Perfect. Can't tell if that's deliberate.
PostgreSQL source code for example, has a foreach() macro that loops thru a linked list.
At the same time the abstractions provided by C preprocessor are too weak for significant productivity gain via elaborate DSL route.
It's better to rather stick to universally understood vocabulary than force your sometimes buggy, often inflexible language upon others.
That being said, C Preprocessor behavior is pretty simple once you know it, and modern compilers are pretty good these days about helping you see what happens when you have an error in your macro-expanded code. You're right though, good to stick to the universally understood vocabulary.