Show HN: Replace printf() with cool generic print, almost like Python/JS(github.com)
github.com
Show HN: Replace printf() with cool generic print, almost like Python/JS
https://github.com/exebook/generic-print
29 comments
The Google way of doing this is absl::StrCat() [1].
[1] https://abseil.io/docs/cpp/guides/strings
[1] https://abseil.io/docs/cpp/guides/strings
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.
With modern C++ you can use the {fmt} library, which is so awesome it became part of the C++20 standard as `std:: format`.
YES! Good use for __builtin_types_compatible_p finally
Great idea. Now post it somewhere other than from the NSA/Microsoft walled garden.
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.
Then read the clang builtins, much better. With clang you can do C++ stuff, not only _Generic, but overloadable. Which is useful for arbitrary struct support for this problem. https://clang.llvm.org/docs/AttributeReference.html#overload...
gcc's compile-time expression chooser is also severely limited compared to clang.
gcc's compile-time expression chooser is also severely limited compared to clang.
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.
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:
strbuf_format(
&buf, "Hello, {1}! 2 + 5 = {0} {{brace}}\n",
2 + 5, "World");
strbuf_format(&buf, "Number = {:#010x}\n", 0xfeed);
strbuf_format(&buf, "Grouped = {:,}\n", 314159265358979323);
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:
format("x = {} + {}", 2, 3u);
// Turns into something like…
format_impl_("x = {} + {}",
// 2 = count
2 | (kFormatInt << 8) | (kFormatUnsigned << 12),
2, 3u);
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.Hello, thank you for sharing, do you have a github repository for this?
I may extract it into a GitHub repo, right now it’s part of a much larger repo which is closed-source.
I’d also be interested in seeing this in its own repo, if only for educational purposes.
Looks very cool! How does it work, and how fast is it?
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.
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.
Nice idea. You mentioned not testing on a mac: https://imgur.com/a/18hnpsj
It appears to work just fine there. Though, once again I question my decision for a light-blue background for my terminal.
It appears to work just fine there. Though, once again I question my decision for a light-blue background for my terminal.
Thanks for testing!
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?
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.
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:
>>> f"{name=}"
'name=Rodrigo'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 don't know how I missed this, I love it, thank you.
I think printf/format and interpolation/fstrings both have their place, and it's nice when languages support both.
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.