I was reading GCC builtins documentation and suddenly realized I can make a generic print function at which you can throw variables of arbitrary types and sizes, even arrays! It is super cool, almost like console.log() in JavaScript or print in Python.
Thanks, very interesting. Unfortunately my main compiler is TinyC so I am limited to those extensions that it supports, although TinyC's community is open to ideas and may be they can add missing bits eventually.
So… I’ve done this. I ported the Python formatting system to C. I did not use any GNU extensions like “a...” in macros or any of the __builtin. It turns out you can do this with regular variadic macros and _Generic, which is standard C these days. By “standard C” I am talking about C11.
This works. It is type-safe. I have not released it because I think it’s a bit… well, painful to use. I’m sure my implementation is a bit different from the one posted, but the key insights are:
1. You can write a variadic macro that figures out how many arguments it was passed in __VA_ARGS__,
2. You can use this variadic macro to paste the number of arguments into the name of another macro which is invoked using the ## preprocessor concatenation operator,
3. You can dispatch on the argument types using _Generic.
The limitations are:
- You have to pick a maximum number of arguments and hard-code that maximum into your library (in order for #1 to work above).
- Macro-expansion makes the error messages a bit crazy and hard to read, like C++, but worse.
- _Generic is rather user-hostile when there is an error.
- Partitioning integral types with _Generic is probably the right way to do things. Unlike C++, there is no implicit casting. It is not obvious how to partition the integral types. I ended up using char, short, int, long, and long long, plus unsigned versions of each, and finally signed char (because char != signed char).
I will say that my version is probably a little more efficient at the call site, since it packs the argument types as bits into integer arguments. For example, it’s something like this:
So the type information for up to 14 arguments is packed into one additional argument, on 64-bit systems. I use code generation for the macros to handle as many arguments as I like, and there is also 32-bit support.
Did not check the speed. I want to use it during debugging/developing so speed was not something I was thinking about.
It is not a very elegant solution thanks to limitations of preprocessor __VAR_ARGS__. Basically there is a compiler builtin function __builtin_types_compatible_p() that checks if the argument is of a given type, then there is another builtin __builtin_choose_expr() that can do something with the result of the former. Using those two builtins I construct an array of type information and pass it to the actual printing function which is using <stdarg.h> and the array of type information to print stuff.
As for your terminal blue background it looks gorgeous, maybe you just need to tweak the palette a little bit to make sure at least 16 basic colors are contrast enough. Btw what is the meaning of :28 in your prompt? Is this amount of files changed or a revision number?
Thanks! I tend to go a little overboard with prompts. I should tweak the base colors.
The number is the number of revisions. Helps me track how "old" different copies of the same repo is. The star next to it denotes changes (in this case the a.out)
The only other things this prompt does right now is show an error code if a command exits with one, and changes to "can't miss it" red when run as root. For me, this is a minimal prompt, I oscillate between a lot more detail, and something closer to a standard installation.
Neat, but it honestly feels like a step backwards for me. I don't pass multiple parameters into print() or console.log() for Python or JS. I use f-strings in Python 3, .format() in Python 2, and template strings in JS. They just read better to me.
There’s a nice syntactic sugar for debugging with f-strings: if you place an equal sign, it will print both the variable name and the value. For example:
Thanks for bringing attention to this! That has bugged me a lot when debugging in different languages and I only stumbled over it recently when reading the Python docs.
I'm not a fan of the hackery used to skip an initial count or trailing 0/NULL.
If you are going to use macro hacks to accomplish this, why not do this:
`#define print(...) _print( 50, __VA_ARGS__, NULL )` then have _print look for the trailing NULL? It's much cleaner.
Nevermind; trailing NULL method would require the size of the NULL to be the largest item you could push onto the stack.
I was picturing it would work, and it would, until you pass in a "format string" which happens to have a single byte 0 in it's address in memory. So it would seem to work until it randomly explodes.
In the case of this particular example, the pattern of args is [ [ format_str1, arg1, arg2 ] x N ]. On a 64-bit platform, the format string would be a 8 byte pointer. So long as one can live with a trailing 8 byte NULL pointer, it would work ok.
C++ of course can have better results because of constructors and templates, my code is for C and I honestly was sure, and for many years until yesterday, that this kind of sugar is basically impossible to achieve in C.
Edit: looking more into it, the header is rather unhygienic: if I include print.h from multiple .c files I break ODR, which is undefined behavior. You have variable definitions in a header which is not good. You should guard those variables with a preprocessor define (something like DECLARE_PRINT_VARIABLES or similar) so I can then #define that to 1 in one and only one .c file and then we don’t break ODR.
29 comments
[ 2.5 ms ] story [ 72.0 ms ] threadgcc's compile-time expression chooser is also severely limited compared to clang.
1. You can write a variadic macro that figures out how many arguments it was passed in __VA_ARGS__,
2. You can use this variadic macro to paste the number of arguments into the name of another macro which is invoked using the ## preprocessor concatenation operator,
3. You can dispatch on the argument types using _Generic.
The limitations are:
- You have to pick a maximum number of arguments and hard-code that maximum into your library (in order for #1 to work above).
- Macro-expansion makes the error messages a bit crazy and hard to read, like C++, but worse.
- _Generic is rather user-hostile when there is an error.
- Partitioning integral types with _Generic is probably the right way to do things. Unlike C++, there is no implicit casting. It is not obvious how to partition the integral types. I ended up using char, short, int, long, and long long, plus unsigned versions of each, and finally signed char (because char != signed char).
I will say that my version is probably a little more efficient at the call site, since it packs the argument types as bits into integer arguments. For example, it’s something like this:
So the type information for up to 14 arguments is packed into one additional argument, on 64-bit systems. I use code generation for the macros to handle as many arguments as I like, and there is also 32-bit support.It is not a very elegant solution thanks to limitations of preprocessor __VAR_ARGS__. Basically there is a compiler builtin function __builtin_types_compatible_p() that checks if the argument is of a given type, then there is another builtin __builtin_choose_expr() that can do something with the result of the former. Using those two builtins I construct an array of type information and pass it to the actual printing function which is using <stdarg.h> and the array of type information to print stuff.
so weird.
It appears to work just fine there. Though, once again I question my decision for a light-blue background for my terminal.
As for your terminal blue background it looks gorgeous, maybe you just need to tweak the palette a little bit to make sure at least 16 basic colors are contrast enough. Btw what is the meaning of :28 in your prompt? Is this amount of files changed or a revision number?
The number is the number of revisions. Helps me track how "old" different copies of the same repo is. The star next to it denotes changes (in this case the a.out)
The only other things this prompt does right now is show an error code if a command exits with one, and changes to "can't miss it" red when run as root. For me, this is a minimal prompt, I oscillate between a lot more detail, and something closer to a standard installation.
It wasn’t added until Python 3.8 (f-strings were new in 3.6).
If you are going to use macro hacks to accomplish this, why not do this: `#define print(...) _print( 50, __VA_ARGS__, NULL )` then have _print look for the trailing NULL? It's much cleaner.
Nevermind; trailing NULL method would require the size of the NULL to be the largest item you could push onto the stack.
I was picturing it would work, and it would, until you pass in a "format string" which happens to have a single byte 0 in it's address in memory. So it would seem to work until it randomly explodes.
In the case of this particular example, the pattern of args is [ [ format_str1, arg1, arg2 ] x N ]. On a 64-bit platform, the format string would be a 8 byte pointer. So long as one can live with a trailing 8 byte NULL pointer, it would work ok.
[1] https://abseil.io/docs/cpp/guides/strings
In my header-only library I have different tricks to detect literal POD types. But portable. In your case unportable is fine.
Edit: looking more into it, the header is rather unhygienic: if I include print.h from multiple .c files I break ODR, which is undefined behavior. You have variable definitions in a header which is not good. You should guard those variables with a preprocessor define (something like DECLARE_PRINT_VARIABLES or similar) so I can then #define that to 1 in one and only one .c file and then we don’t break ODR.