Language:

en_US

switch to room list switch to menu My folders
🧠 #5 - Nerding Out, Part 2

🧠

It’s the halfway point of the month, and that means another newsletter! This one’s going to be focused exclusively on some techniques in C that leverage some macro fun to create a generic “print” kind of macro, that prints any kind of object we get. (Or, at least, a reasonable default!)

But before that…

⭐ CoSy

Trainers are being locked in! We’ve got a handful of trainers for

  • Rust (and are tentatively working for one with Embedded Rust);
  • We’re working to confirm 3 C and C++ trainers;
  • Potentially, 2 Zig trainers;
  • and, two more Special Training in Systems Programming!

We’re getting pretty excited and hope that in the coming month we’ll get to release The Big Lineupβ„’ if they confirm! That news will drop on the website with much fanfare 🎺!

And now…

Multi-argument, type-safe mechanisms in C

It’s time to get into it! First, we need to review what we picked up from a month ago.

Recap

  • We learned that Compound Literals, with the form (type[]){ value }, can be used to give arguments passed to a function longevity.
  • We learned that _Generic is an expression, and each branch of it must not only have the same resulting type, but every branch of the _Generic must compile perfectly for every given value placed into the _Generic.
  • We learned that we can put this behind some macro calls to make it look nicer!

Here is the resulting code for what we had so far, with a quick demonstration in Godbolt if you click this link:

enum arg_type {
    arg_type_int,
    arg_type_double,
    arg_type_ptr_char,
    // ... and more...
    arg_type_shrug
};

typedef struct arg_ {
    enum arg_type type;
    void* data;
} arg;

#define GET_ARG_TYPE(value) _Generic(value, \
    int: arg_type_int, \
    double: arg_type_double, \
    char*: arg_type_ptr_char, \
    /* and so on, and so forth... */ \
    default: arg_type_shrug \
)

#define GET_ARG_DATA(value) \
    ((__typeof((value))[]){ (value) })

#define GET_ARG(value) \
    (arg){ GET_ARG_TYPE(value), GET_ARG_DATA(value) }

void process_arg(arg arg0);

int main () {
    int value0 = 1;
    double value1 = 2.0;
    const char* value2 = "3";
    process_arg( GET_ARG(value0) );
    process_arg( GET_ARG(value1) );
    process_arg( GET_ARG(value2) );
    return 0;
}

#include <stdio.h>

void process_arg(arg arg0) {
    switch (arg0.type) {
        case arg_type_int:
            {
                // points at a single integer
                int value = *(int*)arg0.data;
                printf("%d", value);
            }
            break;
        case arg_type_double:
            {
                // points at a single double
                double value = *(double*)arg0.data;
                printf("%f", value);
            }
            break;

        case arg_type_ptr_char:
            {
                // points at a character string
                char* value = *(char**)arg0.data;
                printf("%s", value);
            }
            break;
        /* and so on, and so forth... */
        default:
            break;
        }
}

Now, from here, we’re going to build towards what we promised in the last January newsletter:

int main (int argc, char*  argv[]) {
    process_args(1, 2.0, "3");
    process_args(0x4, '5');
    process_args((char)0x67);
    return 0;
}

Step 0: Expanding Macros, in Macros

The process_args call looks this way because it is a macro that hides the deduction of argument types and data. That part is easy: we just need to call the GET_ARG macro from what we have:

#define process_args(arg0) \
    process_arg(GET_ARG(arg0))

This is all fine and good, until we get to the stage where we need to expand it over 1 or more arguments. To do this we use the variable arguments for macros, the ... syntax:

#define process_args(...) \
    process_arg(GET_ARG(__VA_ARGS__))

While we might hope this produces 1 GET_ARG for each expanded element, what this in-fact does is place all the arguments in side 1 GET_ARG(...) macro invocation which is all then crammed into one process_arg function call. Of course, none of this works out so the compiler freaks out and we’re back to the start!

So, we need a way to:

  • Pass in multiple arguments (__VA_ARGS__)
  • Call the macro GET_ARG on each individual element, not the whole blob
  • Call the function process_arg or some equivalent for it.

This is going to require a bit of surgery, to both the function calls we have to make and the macro itself. We’ll start with the modifications we need to make to the function call to start.

Step 0, Redo: A Multi-Argument Function Call

First up, the function call process_arg itself. That function is fine for processing individual arguments, but we need more so we can process multiple arg functions. The way to do this is to make a top-level function that can take a variable number of arguments, using the “var_args” construct:

#include <stdarg.h>

void process_all_args (int marker_argument, ...) {
    va_list all_args;
    va_start(all_args, marker_argument);
    /* Processing Here! */
    va_end(all_args);
}

One of the things that makes variable arguments (va_args, as it’s known) work with va_start and va_end macros are 2 things:

  • Knowing the # of arguments you are going to process and use, exactly.
  • Having an argument that is not part of the ... that can be used to “start” the list with va_start.

When C standard library functions like printf and friends use the ... syntax and va_args, the format string is both “marker_argument” and the way to inform the va_args on what to do with each argument it comes across. This is slightly detrimental, because it results in code where you write a specific argument indicator in the format string (such as "%d") when you meant a different kind of indicator (such as "%f") and thusly invoke undefined behavior when it does not match:

int some_integer = 0xAAAA;
printf("%s", some_integer); // uh oh!
printf("%d %f", some_integer); // uh oh, not enough arguments!

Compilers have gotten good at catching these cases for printf with warnings, but this does not really scale up very well for common library developers like us who don’t have the might of the Standard Library behind us. We’ve solved this problem, however, with the arg structure and process_arg function! The type is stored in arg, and we get a compound literal holding the data we can print out. So, we just need to go over each of these arg structures, one at a time, from the va_list:

#include <stdarg.h>

void process_all_args (int marker_argument, ...) {
    va_list all_args;
    va_start(all_args, marker_argument);
    while (1) {
        arg current_arg = va_arg(all_args, arg);
        process_arg(arg);
    }   
    va_end(all_args);
}

Awesome! But…

Step 1: When Do We Stop??

One of the things about printf, formatting strings, and such functions is that there is always some indicator about how many arguments there are, or when to stop processing. Right now, we have an infinite while loop that will keep pulling arguments, forever. That’s no good! In order to get around this, we will make sure there is always a last argument in our list that is an arg, but has two special values:

  • the void* data; member will be set to NULL; and,
  • the arg_type will be arg_type_shrug.

This will aid us in making sure we know when to stop. And thusly, we can use it like so:

/* code from the initial
   part of the article */
#include <stdarg.h>

void process_all_args (int marker_argument, ...) {
    va_list all_args;
    va_start(all_args, marker_argument);
    while (1) {
        arg current_arg = va_arg(all_args, arg);
       if (arg.data == NULL && arg.type == arg_type_shrug) {
           // exit!
           break;
       }
        process_arg(arg);
    }   
    va_end(all_args);
}

int main (int argc, char*  argv[]) {
    process_all_args(
        // marker argument
        0,
        // actual arguments
        GET_ARG(1), GET_ARG("\n2\n"), GET_ARG(3.0),
        // "no more, we are done" marker
        (arg){ arg_type_shrug, NULL }
    );
    return 0;
}

If you run this code, unmodified, you might get some printouts and then some garbage, or you might get a Program Return Code of 139.

Return Code 1…39 ???

Yes! If you get it, 139 means the program has encountered an I/O error, and is shorthand for “something went really wrong trying to read or write this data”. This is mostly because we need to use a special trick called l-value conversion in order to make the string literal "\n2\n" be recognized by our GET_ARG_DATA macro. Namely, __typeof("\n2\n") deduces "\n2\n" to be a type of const char[4], or a whole array.

This matters, because we use __typeof with our GET_ARG_DATA that generates a compound literal. When we unwind that argument on the other side inside the arg_type_ptr_char case value for our switch processing statement, we just expect a char*. This essentially means we’re reading the data wrong, and shenanigans ensue! 😱

We need to somehow get const char[N] to decay to const char*, so it gets held onto properly. We also need it so we can keep the types for other things! It turns out there is a way:

Step 1.5: L-value Conversions!

Yes, it turns out the C Standard has exactly a mechanism for this. It’s called l-value conversions, and it’s a small little feature that decays arrays to pointers and strips qualifiers (const, volatile, _Atomic, etc.) from object types. One way to trigger an l-value conversion is to do a cast, but that’s a bit difficult to expression in our GET_ARG_DATA macro. Another way is to trigger it through other means in the language, like invoking the decay as part of using C’s native comma operator with a void-banished expression. That ends up looking like this:

#define GET_ARG_DATA(value) \
    ((__typeof((void)0, (value))[]){ (value) })

This will prevent the Compound Literal from being stored as an array type and instead make sure the type is a const char* instead. It’s a tad esoteric and a little whacky, in all perfect honestly, but it’s the best tool we have available for this. Note that we are still using __typeof, the extension name for the Standard Feature, because there is still work to do to approve the Standard Feature for the next revision of the C Standard.

“Why not do this for GET_ARG_TYPE, too?”

Good question! It turns out that _Generic, before it starts doing type-matching, will always perform an l-value conversion on the passed-in value. That means it will “decay” arrays to the pointer type (const char[N] becomes const char*). For the compound literal, we need to lift the value up more directly so it matches.

WHEW, that was a lot of work and explanation! So, now we can start on the juicy part of the newsletter: working with variadic macros…

Next Time!

Ooooh, we know, so sorry! If you’ve read through all the way until now, we’re sure you were super excited for more! But, this newsletter is already mega long, and you need a break! At the end of the month, we’ll finally settle the score and make sure you have everything you need to write wonderful, nice-looking C that almost feels as good as C++ and its templates, without the shenanigans!

A complete listing of everything we’ve done, so far, is down below and at this link.

We hope you found this enlightening. We promise we’ll show you the secret to __VA_ARGS__ expansion and other things next time, for sure!!

β€” Shepherd’s Oasis πŸ’™

P.S. A lot of people have been saying this month things like “March Madness”, which got us excited to get involved. Unfortunately, it was a little awkward; we expected chaos but the procession was way too organized!

#include <stdio.h>
#include <stdarg.h>

enum arg_type {
    arg_type_shrug,
    arg_type_int,
    arg_type_double,
    arg_type_ptr_char,
    arg_type_ptr_void
};

typedef struct arg_ {
    enum arg_type type;
    void* data;
} arg;

#define GET_ARG_TYPE(value) _Generic(value, \
    int: arg_type_int, \
    double: arg_type_double, \
    char*: arg_type_ptr_char, \
    const char*: arg_type_ptr_char, \
    void*: arg_type_ptr_void, \
    /* and so on, and so forth... */ \
    default: arg_type_shrug \
)

#define GET_ARG_DATA(value) \
    ((__typeof((void)0, (value))[]){ (value) })

#define GET_ARG(value) \
    (arg){ GET_ARG_TYPE(value), GET_ARG_DATA(value) }

void process_arg(arg arg0);

void process_all_args(int marker_argument, ...);

#define process_args(...) \
    process_all_args(0, \
    /* To Be Discovered Next Time!! */, \
    (arg){ arg_type_shrug, NULL })

int main () {
    process_all_args( 0, GET_ARG(1), GET_ARG("\n2\n"), GET_ARG(3.0), (arg){ arg_type_shrug, NULL } );
    return 0;
}

/* implementation of arg processing! */

void process_all_args(int marker_argument, ...) {
    va_list all_args;
    va_start(all_args, marker_argument);
    while (1) {
        arg current_arg = va_arg(all_args, arg);
        if (current_arg.data == NULL
           && current_arg.type == arg_type_shrug) {
           // exit!
           break;
        }
        process_arg(current_arg);
    }
    va_end(all_args);
}

void process_arg(arg arg0) {
    switch (arg0.type) {
        case arg_type_int:
            {
                // points at a single integer
                int value = *(int*)arg0.data;
                printf("%d", value);
            }
            break;
        case arg_type_double:
            {
                // points at a single double
                double value = *(double*)arg0.data;
                printf("%f", value);
            }
            break;

        case arg_type_ptr_char:
            {
                // points at a character string
                char* value = *(char**)arg0.data;
                printf("%s", value);
            }
            break;
        /* and so on, and so forth... */
        default:
            {
                void* value = arg0.data;
                printf("(unknown type!) %p", value);
            }
            break;
    }
}


https://buttondown.email/Soasis/archive/202103-5-nerding-out-part-2/

Posted by rss <> on Mon Mar 15 2021 21:46:19 UTC
0 comments 0 new | permalink
πŸ”‚ #6 - Magical Macros

πŸ”‚

It's the start of the month again, and that means another newsletter! ... Okay, it's not exactly the start of the Month, but we were deliberately avoiding April 1st because... well. While we enjoy the good Mom Joke or Dad Pun here, sometimes people think it's alright to just be edgy and cruel with their humor!

On the bright side, there's no joke here because it's finally going to be the end of the 3-part Newsletter series! This one teaches you how to make a somewhat nice-feeling interface in C for handling various different kinds of arguments. It has a few holes that may be patched in future standards versions, but we're pretty excited for it! Bur first, let's go over a little recap.

Previously...

In the last 2 letters, we established that we could:

  • Use "va_args" (the ... at the end of a function declaration) to handle any number of arguments;
  • That we could use a special "last" argument marker to know when to stop processing arguments rather than depending on, say, a format string or an explicit count passed into the argument;
  • And, how to properly capture string literals and other data using l-value conversion.

With all of that under our belts, it's time to get started on the real magic that would make the Macromancer himself proud!

Let's handle an arbitrary sequence of arguments/types in a function-like macro using ... and __VA_ARGS__ in the preprocessor.

__VA_ARGS__

While the mechanism for handling variable number of arguments in C is clumsy and generally type-unsafe (see: the number of compiler warnings around using printf with the wrong format specifiers for e.g. an int or a double), we can force that type safety by falling back onto a technique called Macro Generic Programming, or MGP for short. MGP allows us to give a simple syntax and bury the completing of type checking and other shenanigans into the preprocessor, giving us flexible interfaces even in a programming language like C that lacks the strong generic programming support found in other languages.

We already have been using some MGP to create our GET_ARG macro:

/* ... */

#define GET_ARG_TYPE(value) _Generic(value, \
    int: arg_type_int, \
    double: arg_type_double, \
    char*: arg_type_ptr_char, \
    const char*: arg_type_ptr_char, \
    void*: arg_type_ptr_void, \
    /* and so on, and so forth... */ \
    default: arg_type_shrug \
)

#define GET_ARG_DATA(value) \
    ((__typeof((void)0, (value))[]){ (value) })

#define GET_ARG(value) \
    (arg){ GET_ARG_TYPE(value), GET_ARG_DATA(value) }

The GET_ARG macro has been producing both the type and the data. Now, we just need to use it on the arguments we receive, in a recursive manner! Like mentioned in the last newsletter, this is -- unfortunately -- not something we are capable of doing:

#define process_args(...) \
    process_arg(GET_ARG(__VA_ARGS__))

This does not call GET_ARG for each value. So we need to call GET_ARG on each value, one at a time. We re-worked our base function to be capable of this in the last newsletter:

void process_all_args (int marker_argument, ...);

And now it's time to use {insert some magical implementation technique here} to allow for something that looks like this:

#define process_args(...) \
    process_all_args(/* marker */ 0xAAAA, \
        FOR_ALL_ARGS(GET_ARG, __VA_ARGS__))

So... let's do it!

FOR_ALL_ARGS, in the Preprocessor

We won't call it FOR_ALL_ARGS, because we're not going to create a "general purpose" invoker of macros. What we will do, though, is write something that expands all the tokens out for us, and then start working from there to invoke our GET_ARG one at a time. We do not really have the power to invoke things in a loop or in a while statement, since there exists no such construct in the preprocessor. But, you can fake it with a pseudo-recursion. We say "pseudo" here, because it's not really true recursion. Instead, it's kind of like a hand-unrolled loop or a sequentially daisy-chained set of wires, really:

#define PROCESS_ARG_LAST() (arg){ arg_type_shrug, NULL }

#define PROCESS_ARG0()         PROCESS_ARG_LAST() // and we're done!!
#define PROCESS_ARG1(val)      GET_ARG(val), PROCESS_ARG0()
#define PROCESS_ARG2(val, ...) GET_ARG(val), PROCESS_ARG1(__VA_ARGS__)
#define PROCESS_ARG3(val, ...) GET_ARG(val), PROCESS_ARG2(__VA_ARGS__)
#define PROCESS_ARG4(val, ...) GET_ARG(val), PROCESS_ARG3(__VA_ARGS__)
#define PROCESS_ARG5(val, ...) GET_ARG(val), PROCESS_ARG4(__VA_ARGS__)
#define PROCESS_ARG6(val, ...) GET_ARG(val), PROCESS_ARG5(__VA_ARGS__)
#define PROCESS_ARG7(val, ...) GET_ARG(val), PROCESS_ARG6(__VA_ARGS__)

#define PROCESS_ARGS_SELECT(inv1, inv2, inv3, inv4, inv5, inv6, inv7, INVOKE_THIS_ONE, ...) \
    INVOKE_THIS_ONE

#define process_args(...) \
    process_all_args( 0xAAAA, PROCESS_ARGS_SELECT(__VA_ARGS__, \
        PROCESS_ARG7, PROCESS_ARG6, PROCESS_ARG5, \
        PROCESS_ARG4, PROCESS_ARG3, PROCESS_ARG2, \
        PROCESS_ARG1, PROCESS_ARG0)(__VA_ARGS__))

"... Huh?!"

Okay, so! This looks quite a bit complicated, but I promise you it's actually a really simple concept rooted in the fact that we can only have X number of maximum arguments. process_args is our top-level invocation. It's job is to apply GET_ARG to everything it receives, so we can do all that compound-literal generation of the struct arg objects we need to do the printing. Now, you may be wondering: "Why do you use __VA_ARGS__ in the PROCESS_ARG_SELECT macro invocation, AND in at the end in the parentheses?". This is where the trick comes in, and why we explicitly list out the PROCESS_ARG7, PROCESS_ARG6, etc. values. The goal of the PROCESS_ARG_SELECT call is to pick the right macro to invoke and start the chain of GET_ARGs! This is achieved by the dumping of the macro arguments before us, and then using the offset position of the given PROCESS_ARG[n] macro to determine which to call.

An Example

Let's look at the hypothetical expansion of these, if we were a compiler, and why this would work. So, consider the call

process_args(1);

Then that would expand, at first, to something like this:

process_all_args(0xAAAA, PROCESS_ARGS_SELECT(1,
        PROCESS_ARG7, PROCESS_ARG6, PROCESS_ARG5,
        PROCESS_ARG4, PROCESS_ARG3, PROCESS_ARG2,
        PROCESS_ARG1, PROCESS_ARG0)(1));

So far, so good. Now, how does PROCESS_ARGS_SELECT expand? Well, the goal is that it picks the right macro to expand to. If you expand out just PROCESS_ARGS_SELECT, you end up with (annotated to drive home the point):

PROCESS_ARGS_SELECT(1 /* inv7 */,  PROCESS_ARG7 /* inv6 */,
        PROCESS_ARG6 /* inv5 */, PROCESS_ARG5 /* inv4 */,
        PROCESS_ARG4 /* inv3 */, PROCESS_ARG3 /* inv2 */,
        PROCESS_ARG2 /* inv1 */,
        PROCESS_ARG1 /* INVOKE_THIS_ONE */,
        PROCESS_ARG0 /* ... */)

Which, removing annotations and doing the call, becomes:

process_all_args(0xAAAA, PROCESS_ARG1(1));

Aha! See, PROCESS_ARGS_SELECT essentially "pushes" the right macro to invoke (one of the PROCESS_ARGS[n] macros) into the INVOKE_THIS_ONE position. That's why we call it SELECT; it selects the right macro to invoke! So, finally, the function call ends up looking like this:

process_all_args(0xAAAA,
    (arg){ arg_type_shrug, (int[]){ 1 } },
    (arg){ arg_type_shrug, NULL } );

And, when called, you should see this output:

1

Boom!

And that's it! That's all the magic! This generalizes to 2, 3, 4, 5, etc. arguments! We have achieved a type-safe, variadic call in a C API, that keeps its arguments alive with the power of compound literals! Someone working with us is working on getting __typeof to not be a widely-implemented extension and instead a normal part of things. But, there are some problems with this setup that are, quite literally, impossible to solve in Standard C even if we take the __typeof for granted.

__VA_ARGS__ and zero arguments

You'll notice that nowhere during this exercise have we run through the usage of process_args(), where it's called with no arguments. This is, unfortunately, intentional, because it can't work. The biggest problem with ... and __VA_ARGS__ in macros is that the __VA_ARGS__ and the paired ... construct are not allowed to have no arguments passed in it:

If there is a ... in the identifier-list in the macro definition, then the trailing arguments, including any separating comma preprocessing tokens, are merged to form a single item: the variable arguments. The number of arguments so combined is such that, following merger, the number of arguments is one more than the number of parameters in the macro definition (excluding the ...).

β€” Β§6.10.3 Macro replacement, Semantics, paragraph 12, C Standard Working Draft

There must always be 1 or more arguments to the function for the ... part. This unfortunate fact is only resolved by a paper that's been fixed in C++20 with __VA_OPT__; the C Committee still needs to move the required N2610 forward. If you'd like to see this problem solved so that C is not behind on C++ with the preprocessor, we do encourage you to e-mail the author with supporting words, and maybe ask to lend a helping hand!

More Fundamental: a maximum of 7

Right now, if you use this code from the article, directly, it only gives 7 arguments maximum. That's a cute number, but likely to fall apart with rigorous use. The C++ Boost.Preprocessor library typically allows for up to 64, but be careful: bigger macros can cost more for compile time. If you're from Rust or C++, you're likely already used to gigantic compile times and macros being slightly complicated won't make you blink an eye and will still be faster than procedural Rust macros, Rust traits, and/or C++ templates.

We'll have some brand new knowledge for you, next newsletter! A full code listing is available on Godbolt for you to play with, and is shown below.

Until next time!

β€” Shepherd's Oasis πŸ’™

P.S.: We would've put a joke here, but honestly we're a little "fooled out" from the exhausting, too-edgy jokes from April Fool's Day. Just have a good one, okay? 😊

#include <stdio.h>
#include <stdarg.h>

enum arg_type {
    arg_type_shrug,
    arg_type_int,
    arg_type_double,
    arg_type_ptr_char,
    arg_type_ptr_void
};

typedef struct arg_ {
    enum arg_type type;
    void* data;
} arg;

#define GET_ARG_TYPE(value) _Generic(value, \
    int: arg_type_int, \
    double: arg_type_double, \
    char*: arg_type_ptr_char, \
    const char*: arg_type_ptr_char, \
    void*: arg_type_ptr_void, \
    /* and so on, and so forth... */ \
    default: arg_type_shrug \
)

#define GET_ARG_DATA(value) \
    ((__typeof((void)0, (value))[]){ (value) })

#define GET_ARG(value) \
    (arg){ GET_ARG_TYPE(value), GET_ARG_DATA(value) }

void process_arg(arg arg0);

void process_all_args(int marker_argument, ...);

#define PROCESS_ARG_LAST() (arg){ arg_type_shrug, NULL }

#define PROCESS_ARG0()         PROCESS_ARG_LAST()    // and we're done!!
#define PROCESS_ARG1(val)      GET_ARG(val), PROCESS_ARG0()
#define PROCESS_ARG2(val, ...) GET_ARG(val), PROCESS_ARG1(__VA_ARGS__)
#define PROCESS_ARG3(val, ...) GET_ARG(val), PROCESS_ARG2(__VA_ARGS__)
#define PROCESS_ARG4(val, ...) GET_ARG(val), PROCESS_ARG3(__VA_ARGS__)
#define PROCESS_ARG5(val, ...) GET_ARG(val), PROCESS_ARG4(__VA_ARGS__)
#define PROCESS_ARG6(val, ...) GET_ARG(val), PROCESS_ARG5(__VA_ARGS__)
#define PROCESS_ARG7(val, ...) GET_ARG(val), PROCESS_ARG6(__VA_ARGS__)

#define PROCESS_ARGS_SELECT(inv1, inv2, inv3, inv4, inv5, inv6, inv7, INVOKE_THIS_ONE, ...) \
    INVOKE_THIS_ONE

#define process_args(...) \
    process_all_args( 0xAAAA, PROCESS_ARGS_SELECT(__VA_ARGS__, \
        PROCESS_ARG7, PROCESS_ARG6, PROCESS_ARG5, \
        PROCESS_ARG4, PROCESS_ARG3, PROCESS_ARG2, \
        PROCESS_ARG1, PROCESS_ARG0)(__VA_ARGS__))

int main () {
    process_args( NULL, "\n", 1, "\n2\n", 3.0 );
    return 0;
}

/* implementation of arg processing! */

void process_all_args(int marker_argument, ...) {
    va_list all_args;
    va_start(all_args, marker_argument);
    while (1) {
        arg current_arg = va_arg(all_args, arg);
        if (current_arg.data == NULL
           && current_arg.type == arg_type_shrug) {
           // exit!
           break;
        }
        process_arg(current_arg);
    }
    va_end(all_args);
}

void process_arg(arg arg0) {
    switch (arg0.type) {
        case arg_type_int:
            {
                // points at a single integer
                int value = *(int*)arg0.data;
                printf("%d", value);
            }
            break;
        case arg_type_double:
            {
                // points at a single double
                double value = *(double*)arg0.data;
                printf("%f", value);
            }
            break;
        case arg_type_ptr_char:
            {
                // points at a character string
                char* value = *(char**)arg0.data;
                printf("%s", value);
            }
            break;
        case arg_type_ptr_void:
            {
                // points at a character string
                void* value = *(void**)arg0.data;
                printf("%p", value);
            }
            break;
        /* and so on, and so forth... */
        default:
            {
                void* value = arg0.data;
                printf("(unknown type!) %p", value);
            }
            break;
    }
}


https://buttondown.email/Soasis/archive/6-magical-macros/

Posted by rss <> on Tue Apr 06 2021 02:33:22 UTC
0 comments 0 new | permalink