Advanced Metaprogramming in C: A select statement (250bpm.com)

mckirk 2 years ago

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_at 2 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...

quelsolaar 2 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.

jbboehr 2 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].

[0]: https://doc.rust-lang.org/reference/procedural-macros.html [1]: https://github.com/dtolnay/cargo-expand

saghm 2 years ago

> 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.

LanternLight83 2 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.

kaba0 2 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 :)

rrdharan 2 years ago

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…

pcr910303 2 years ago

AFAIK It’s http://libdill.org now :)

s-video 2 years ago

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.

jstimpfle 2 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:

    #define TIMED_SECTION_(name, t) FOR_SCOPE(Timer t = start_timer(); print_passed_time(name, t))
    #define TIMED_SECTION(name) TIMED_SECTION_(name, UNIQUE_NAME())
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!
kaba0 2 years ago

I don’t know, I would much rather prefer a sane metaprogramming system instead of this unhygienic mess of a hack.

jstimpfle 2 years ago

Wouldn't we all. Now show me how to do one that can do all the same things.

kaba0 2 years ago

C++’s templates, const functions, etc. Rust’s macro system.

C just refuses to improve, even though there are a huge space for those.

jstimpfle 2 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".

kaba0 2 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.

jstimpfle 2 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

    DECLARE_CHUNKLIST(My_Foo_Type) foo_chunklist;
    CHUNKLIST_PUSH(&foo_chunklist) = make_foo();
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.

WalterBright 2 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.

Spivak 2 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.

WalterBright 2 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.

LanternLight83 2 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)

tpoacher 2 years ago

"A witty quote proves nothing."

~ Voltaire

tpoacher 2 years ago

you mean, like, assembly?

tpoacher 2 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!

jxy 2 years ago

Using initializer list would be much more readable without the atrocious macros.

jesse__ 2 years ago

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.

david2ndaccount 2 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.

Gibbon1 2 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?

The answer for gnu99 is yes.

   // will barf if fed a pointer
   #define sizeof_array(arr) \
      (sizeof(arr) / sizeof((arr)[0]) \
      + sizeof(typeof(int[1 - 2 * \
      !!__builtin_types_compatible_p(typeof(arr), \
                 typeof(&arr[0]))])) * 0)

   typedef struct
   {
      int x, y;
   } Foo;

   Foo foo[] = 
   {
     { .x = 10, .y = 20},
     { .x = 11, .y = 23}
   };

   foo_cnt = sizeof_array(foo); // foo_cnt == 2
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.

pid_0 2 years ago

undefined