On the one hand, I personally enjoy exercises like this, and the end result is quite pleasing to look at. On the other hand, the whole idea is kind of funny to me.
In my experience, a proclivity to programming serious low-level code in C very much comes with an intense defensive reaction to things like "metaprogramming using the C precompiler". At least I still vividly the remember my co-workers' reaction to the C code I wrote as an intern, when I tried to be clever.
jesse__2 years ago
> programming serious low-level code in C
I mean.. a lot of code that's written in C isn't particularly "low level" in the sense that it's optimizing for runtime or memory in any kind of outrageous way. A lot of C is very normal/straight forward code which can benefit greatly from metaprogramming due to the lack of language-level niceties.
> I still vividly the remember my co-workers' reaction to the C code I wrote as an intern, when I tried to be clever.
That's understandable. I'm highly skeptical of clever code anyone writes, including myself.
I guess what I'm trying to say here is that metaprogramming and being clever aren't mutually intertwined ideas.
comma_at2 years ago
libmill's goal was to be as close to Go as possible. If you look at it in that light, it's an accomplishment.
Then came libdill, which created the whole "structured concurrency" movement and brought new concepts to light that are now being replicated in python, kotlin, java...
quelsolaar2 years ago
I have thought a lot about meta programming and the best way to do it (I participate in the ISO C wg14) and I dislike macros, templets and every other attempt I have seen.
The best solution I have found, disregarding of language, is to use the same language to write a program that generates the code you need.
Then you can compile, and debug them separately using the same workflow, and each program can be clear and simple to read. Its not sexy or clever, but it works and anyone can understand what is going on. Almost always that turns out to be the most important thing.
jbboehr2 years ago
Procedural macros[0] in Rust seem to fit this a bit. Although you don't see the generated code unless you use something like cargo-expand[1].
> The best solution I have found, disregarding of language, is to use the same language to write a program that generates the code you need.
Sounds like a pitch for lisp; I haven't really used any lisp variant much, but I always hear about people saying they basically write a program to generate their program rather than actually just solving the problem directly.
LanternLight832 years ago
Absolutely; Complex macros are frequently written by breaking them up into smaller "actual" functions. The body of the macro just receives it's unevaluated arguments and calls those functions on them, because the only thing that's special about it is when it runs, that it's arguments are initially unevaluated, and that it's output is interpreted (possibly by a compiler) as code. Within the context of the macro's body, it's lisp like any other.
kaba02 years ago
Or zig’s compile time functions.
jesse__2 years ago
I definitely agree. I'm actually in the middle of writing a C metaprogramming runtime (effectively a parser that spits out an AST) which is useful for writing such programs.
Also, unrelated, but I miss watching your streams on Twitch, assuming you're the same quelsolaar :)
libmill and libdill are similar but different projects by the same author. I think he just let the libmill domain expire. What's funny is the site is a GitHub Pages site, so it's still there on the libmill repository, and could be accessed at https://sustrik.github.io/libmill but it's configured to redirect to that expired libmill domain so it's inaccessible.
jstimpfle2 years ago
Metaprogramming should be limited, and metaprogramming in C is quite brittle and almost nobody really understand the execution model of he preprocessor. But, dismissing the preprocessor outright is too quick a conclusion. There are some clever and very practical uses of the preprocessor.
- portability
- generate strings from identifiers
- Insert file and line of the call location (or other context) automatically when calling a function.
- X-macros
- Scopes with exit statement given at beginning,
hacked using "for" statement and a condition that
evaluates to true only the first time. A little bit
brittle when the exit statement must not be skipped (goto, return).
I'm sure there are more, but I'm too tired to think harder... Here is an example of the last category:
Not giving the FOR_SCOPE() and UNIQUE_NAME() macros here for brevity. They add another 3-5 reusable lines. Use like this:
TIMED_SECTION("foo the bar")
{
/* Do stuff, time spent in this scope will be measured and printed on scope exit. */
}
Other more advanced languages can do some of the same things, sometimes more robustly, or not at all (X-macros is probably hard to replace for languages that are not LISP). But it typically comes at a cost in complexity, hard to use type systems, complicated semantics with edge cases... And they only allow you to do what is already built into the language!
kaba02 years ago
I don’t know, I would much rather prefer a sane metaprogramming system instead of this unhygienic mess of a hack.
jstimpfle2 years ago
Wouldn't we all. Now show me how to do one that can do all the same things.
C++’s templates, const functions, etc. Rust’s macro system.
C just refuses to improve, even though there are a huge space for those.
jstimpfle2 years ago
All these come with additional complexity on the syntactic as well as semantic levels. C++ templates are an interesting in-between, between solid and "soft" structure. Not dissimilar to hygienic Lisp macros I think. But all this stuff multiplies complexities of using the language as well as compiler design.
Not saying that other approaches are bad, just that C has some qualities that are underappreciated, or simply not understood by many. In case of the preprocessor, while it's certainly flawed, it's good that it's a mechanism that is completely orthogonal to the language. Which means it doesn't interact with the normal syntax and semantics of the language, isn't a complexity multiplier. There is a single, well-chosen interaction point: the lexical syntax (token stream).
This makes it hard or impossible to implement certain user-defined extensions in a robust way, but also allows you to do many important tasks of factoring out syntactic boilerplate just fine. And you can do some extremely practical hacks with it that other systems just don't let you do at all. In any case, the cost of using it is very "self-contained".
kaba02 years ago
> it's good that it's a mechanism that is completely orthogonal to the language
I don’t know, doing what equals to find and replacing string inside a Programming language (with a bit exaggeration on my part) seems like a terrible terrible idea, even just for the very trivial reason of outputting paired number of brackets. It is also terrible from a readability and maintainability point of view with quite a few insane hacks of hacks done to implement basic functionality in other languages (there is an infamous macro in the linux kernel that is so cleverly hacky that it is on another level).
And all that for nothing, as the language is incapable of expressing even such a basic thing as a generic array/vector-like data structure without a huge indirection, let alone more complex things.
Minimalism is a nice goal, but without abstractibility/expressivity it is useless.
jstimpfle2 years ago
> (there is an infamous macro in the linux kernel that is so cleverly hacky that it is on another level)
Probably that says more about the particular programmer than it says about macros. Also on a Linux kernel scale some tradeoffs are different.
> And all that for nothing, as the language is incapable of expressing even such a basic thing as a generic array/vector-like data structure
Something you often do not do when programming in C. You do not want "vector", reallocating by a constant factor without second thoughts, for "serious" systems programming. Same goes for much of the other stuff that is in STL for example.
And you can in fact design macros in a few lines that allow you to do something like
I.e. In fact no terrible noise involved. The hack employed here is
#define DECLARE_CHUNKLIST(type) union { type *_type; struct { /* chunklist implementation ... */ } _impl; }
which allows the list to carry its own type to support operations like CHUNKLIST_PUSH. For good ergonomics this relies on typeof().
But again, there is a lot of "serious" programming that gets by just fine without any of such hacks. Sometimes you're programming an event handling system where you mostle use fixed-size but dynamically-allocated buffers and push those around. There is very little generic code used here, the functions you write are dealing with concrete flat buffers mostly.
WalterBright2 years ago
If you're using macros for metaprogramming in C, you've outgrown the language and are ready for a more powerful C replacement.
Spivak2 years ago
Unless you need it to be in C because you’re targeting platforms only supported by a C compiler or because you’re writing a library and want different languages to have FFI.
WalterBright2 years ago
With D you can interface seamlessly to C, in both directions. D works fine at presenting a C interface any C code can use.
LanternLight832 years ago
I've never thought about it that way before, and suppose that it applies to any language with a two-way FFI (including more diverse languages like, well, just Guile Scheme off the top of my head, but insure there are more-- I'm sure someone could really benefit from a list of languages that can essentially target C like this)
tpoacher2 years ago
"A witty quote proves nothing."
~ Voltaire
tpoacher2 years ago
you mean, like, assembly?
tpoacher2 years ago
> However, such syntax is ugly and verbose.
Is it just me who thinks this was by far the clearest, cleanest formulation? Everything following it seemed to sacrifice clarity for the sake of cleverness.
Also I really dislike macros that try to disguise themselves as "not macros". Capitalise the crap out if it, make the fact that this is a macro stand out, man!
jxy2 years ago
Using initializer list would be much more readable without the atrocious macros.
And with that said, I fail to see how using an initializer would have any effect on the end result.. unless of course you only read the first 150 words. Then your comment, while unnecessary, would make sense.
david2ndaccount2 years ago
Initializer-lists are most definitely a thing in C and are part of the grammar. Check 6.7.9 of your C standard
struct Foo {
int x, y;
};
struct Foo foo = {1, 2};
// ^ this is an initializer list.
They also include designators, like:
struct Foo foo = {.x=1, .y=2};
The straw man struct initialization at the beginning of the article would be greatly improved with such a construct. Once you are no longer messing with macros, the rest of the article becomes a step too far.
I assume the article was for c89, as c89 did not allow non-constant initializers. Any modern C compiler should compile c99 or newer code though.
Gibbon12 years ago
Your comment made me look at the article. I saw this line
> Can we somehow not require the user to specify the size of the pollset in advance?
One of the reasons why I think the C standards committee needs to be fired is because sizeof_array should have been made part of the language 20 years ago.
jesse__2 years ago
Ahh, now I see what you're saying; that does seem true. Sorry for the snarky comment.
On the one hand, I personally enjoy exercises like this, and the end result is quite pleasing to look at. On the other hand, the whole idea is kind of funny to me.
In my experience, a proclivity to programming serious low-level code in C very much comes with an intense defensive reaction to things like "metaprogramming using the C precompiler". At least I still vividly the remember my co-workers' reaction to the C code I wrote as an intern, when I tried to be clever.
> programming serious low-level code in C
I mean.. a lot of code that's written in C isn't particularly "low level" in the sense that it's optimizing for runtime or memory in any kind of outrageous way. A lot of C is very normal/straight forward code which can benefit greatly from metaprogramming due to the lack of language-level niceties.
> I still vividly the remember my co-workers' reaction to the C code I wrote as an intern, when I tried to be clever.
That's understandable. I'm highly skeptical of clever code anyone writes, including myself.
I guess what I'm trying to say here is that metaprogramming and being clever aren't mutually intertwined ideas.
libmill's goal was to be as close to Go as possible. If you look at it in that light, it's an accomplishment.
Then came libdill, which created the whole "structured concurrency" movement and brought new concepts to light that are now being replicated in python, kotlin, java...
I have thought a lot about meta programming and the best way to do it (I participate in the ISO C wg14) and I dislike macros, templets and every other attempt I have seen.
The best solution I have found, disregarding of language, is to use the same language to write a program that generates the code you need.
Then you can compile, and debug them separately using the same workflow, and each program can be clear and simple to read. Its not sexy or clever, but it works and anyone can understand what is going on. Almost always that turns out to be the most important thing.
Procedural macros[0] in Rust seem to fit this a bit. Although you don't see the generated code unless you use something like cargo-expand[1].
[0]: https://doc.rust-lang.org/reference/procedural-macros.html [1]: https://github.com/dtolnay/cargo-expand
> The best solution I have found, disregarding of language, is to use the same language to write a program that generates the code you need.
Sounds like a pitch for lisp; I haven't really used any lisp variant much, but I always hear about people saying they basically write a program to generate their program rather than actually just solving the problem directly.
Absolutely; Complex macros are frequently written by breaking them up into smaller "actual" functions. The body of the macro just receives it's unevaluated arguments and calls those functions on them, because the only thing that's special about it is when it runs, that it's arguments are initially unevaluated, and that it's output is interpreted (possibly by a compiler) as code. Within the context of the macro's body, it's lisp like any other.
Or zig’s compile time functions.
I definitely agree. I'm actually in the middle of writing a C metaprogramming runtime (effectively a parser that spits out an AST) which is useful for writing such programs.
Also, unrelated, but I miss watching your streams on Twitch, assuming you're the same quelsolaar :)
http://libmill.org/ “is in an active auction”.
https://www.sav.com/auctions/details/1159199/libmill.org
Insert some joke about the dangers of overly abusing the C precompiler…
AFAIK It’s http://libdill.org now :)
libmill and libdill are similar but different projects by the same author. I think he just let the libmill domain expire. What's funny is the site is a GitHub Pages site, so it's still there on the libmill repository, and could be accessed at https://sustrik.github.io/libmill but it's configured to redirect to that expired libmill domain so it's inaccessible.
Metaprogramming should be limited, and metaprogramming in C is quite brittle and almost nobody really understand the execution model of he preprocessor. But, dismissing the preprocessor outright is too quick a conclusion. There are some clever and very practical uses of the preprocessor.
- portability
- generate strings from identifiers
- Insert file and line of the call location (or other context) automatically when calling a function.
- X-macros
- Scopes with exit statement given at beginning, hacked using "for" statement and a condition that evaluates to true only the first time. A little bit brittle when the exit statement must not be skipped (goto, return).
I'm sure there are more, but I'm too tired to think harder... Here is an example of the last category:
Not giving the FOR_SCOPE() and UNIQUE_NAME() macros here for brevity. They add another 3-5 reusable lines. Use like this: Other more advanced languages can do some of the same things, sometimes more robustly, or not at all (X-macros is probably hard to replace for languages that are not LISP). But it typically comes at a cost in complexity, hard to use type systems, complicated semantics with edge cases... And they only allow you to do what is already built into the language!I don’t know, I would much rather prefer a sane metaprogramming system instead of this unhygienic mess of a hack.
Wouldn't we all. Now show me how to do one that can do all the same things.
https://github.com/eudoxia0/cmacro
C++’s templates, const functions, etc. Rust’s macro system.
C just refuses to improve, even though there are a huge space for those.
All these come with additional complexity on the syntactic as well as semantic levels. C++ templates are an interesting in-between, between solid and "soft" structure. Not dissimilar to hygienic Lisp macros I think. But all this stuff multiplies complexities of using the language as well as compiler design.
Not saying that other approaches are bad, just that C has some qualities that are underappreciated, or simply not understood by many. In case of the preprocessor, while it's certainly flawed, it's good that it's a mechanism that is completely orthogonal to the language. Which means it doesn't interact with the normal syntax and semantics of the language, isn't a complexity multiplier. There is a single, well-chosen interaction point: the lexical syntax (token stream).
This makes it hard or impossible to implement certain user-defined extensions in a robust way, but also allows you to do many important tasks of factoring out syntactic boilerplate just fine. And you can do some extremely practical hacks with it that other systems just don't let you do at all. In any case, the cost of using it is very "self-contained".
> it's good that it's a mechanism that is completely orthogonal to the language
I don’t know, doing what equals to find and replacing string inside a Programming language (with a bit exaggeration on my part) seems like a terrible terrible idea, even just for the very trivial reason of outputting paired number of brackets. It is also terrible from a readability and maintainability point of view with quite a few insane hacks of hacks done to implement basic functionality in other languages (there is an infamous macro in the linux kernel that is so cleverly hacky that it is on another level).
And all that for nothing, as the language is incapable of expressing even such a basic thing as a generic array/vector-like data structure without a huge indirection, let alone more complex things.
Minimalism is a nice goal, but without abstractibility/expressivity it is useless.
> (there is an infamous macro in the linux kernel that is so cleverly hacky that it is on another level)
Probably that says more about the particular programmer than it says about macros. Also on a Linux kernel scale some tradeoffs are different.
> And all that for nothing, as the language is incapable of expressing even such a basic thing as a generic array/vector-like data structure
Something you often do not do when programming in C. You do not want "vector", reallocating by a constant factor without second thoughts, for "serious" systems programming. Same goes for much of the other stuff that is in STL for example.
And you can in fact design macros in a few lines that allow you to do something like
I.e. In fact no terrible noise involved. The hack employed here is which allows the list to carry its own type to support operations like CHUNKLIST_PUSH. For good ergonomics this relies on typeof().But again, there is a lot of "serious" programming that gets by just fine without any of such hacks. Sometimes you're programming an event handling system where you mostle use fixed-size but dynamically-allocated buffers and push those around. There is very little generic code used here, the functions you write are dealing with concrete flat buffers mostly.
If you're using macros for metaprogramming in C, you've outgrown the language and are ready for a more powerful C replacement.
Unless you need it to be in C because you’re targeting platforms only supported by a C compiler or because you’re writing a library and want different languages to have FFI.
With D you can interface seamlessly to C, in both directions. D works fine at presenting a C interface any C code can use.
I've never thought about it that way before, and suppose that it applies to any language with a two-way FFI (including more diverse languages like, well, just Guile Scheme off the top of my head, but insure there are more-- I'm sure someone could really benefit from a list of languages that can essentially target C like this)
"A witty quote proves nothing."
~ Voltaire
you mean, like, assembly?
> However, such syntax is ugly and verbose.
Is it just me who thinks this was by far the clearest, cleanest formulation? Everything following it seemed to sacrifice clarity for the sake of cleverness.
Also I really dislike macros that try to disguise themselves as "not macros". Capitalise the crap out if it, make the fact that this is a macro stand out, man!
Using initializer list would be much more readable without the atrocious macros.
Care to elaborate, or better yet link to a gist?
Initializer lists aren't a thing in C, so I assume you're referring to array initialization : https://en.cppreference.com/w/c/language/array_initializatio...
And with that said, I fail to see how using an initializer would have any effect on the end result.. unless of course you only read the first 150 words. Then your comment, while unnecessary, would make sense.
Initializer-lists are most definitely a thing in C and are part of the grammar. Check 6.7.9 of your C standard
They also include designators, like: The straw man struct initialization at the beginning of the article would be greatly improved with such a construct. Once you are no longer messing with macros, the rest of the article becomes a step too far.I assume the article was for c89, as c89 did not allow non-constant initializers. Any modern C compiler should compile c99 or newer code though.
Your comment made me look at the article. I saw this line
> Can we somehow not require the user to specify the size of the pollset in advance?
The answer for gnu99 is yes.
One of the reasons why I think the C standards committee needs to be fired is because sizeof_array should have been made part of the language 20 years ago.Ahh, now I see what you're saying; that does seem true. Sorry for the snarky comment.
undefined