8 ms·
Type-Safe Printf for C
- Animats 5y agoGCC has had printf checking for, what, 20 years?
- AlexanderDhoore 5y agoHow is this safer than enabling all warnings in GCC or clang? 'Type-safe' in this context does not mean that you get more compile errors, but that the format specifier does not need to specify the argument type, but just defines the print format. In fact, format strings with this library will have less compile-time checking (namely none) than with modern compilers for standard printf. This approach is still safer. EDIT The answer is at the bottom apparently. Maybe put that up higher?
- edflsafoiewq 5y ago> There is absolutely no chance to give a wrong format specifier and access the stack (like printf does via stdarg.h) in undefined ways. This is particularly true for multi-arch development where with printf you need to be careful about length specifiers, and you might not get a warning on your machine, but the next person will and it will crash there. I usually need to compile for a few times on multiple architectures to get the integer length correct, e.g., %u vs %lu vs. %llu vs. %zu.
- kevin_thibedeau 5y agoThis is really annoying on architectures like ARM32 where size_t is closely related to unsigned int but uint32_t is long unsigned int and gets flagged as a different type. It becomes a real problem when using a stripped down printf like the one in newlib that doesn't support %zu.
- tinkersleep 5y agoExactly! Or on Windows 64-bit, where 'long' is 32-bit and 'size_t' is 'unsigned long long'.
- GeorgeTirebiter 5y agoThe Real Problem (tm) is: specifiers like 'char' and 'int' etc should not be allowed; they 'should be' things like c8 or i16 or u64 --- that is, specify the #of bits for that dataype in the type specifier. This is what sys/stdint.h is trying to fix. What maybe 'should' happen in C2x is: 'int' is defined as i16, 'long' as i32, 'long long' as i64 etc and then see which programs break. Because it's perfectly OK to have 16-bit 'ints' on a 64-bit arch. (size_t is what you use to deal with architecture-specific chunks). And then remove all this 'int' etc crap from C. (Obv, some 'compat switch' would need to exist, but you get the idea.)
- kevin_thibedeau 5y agoNo that should not happen. Integer types that adapt to the platform word size enhance portability. Nobody wants a 32-bit default int on an 8-bit platform and using uint8_t or uint16_t can introduce performance regressions on wider platforms. The traditional integer types are perfectly suited for scenarios where the exact width doesn't matter and you know the guaranteed minimum is good enough.
- arka2147483647 5y agoI would argue that most code nowdays iplicitly assumes that int is 32bit’s long, and wont work correctly in a 8bit platform anyways. If ’platform size conforming’ ints are used, they probably should be opt-in, instead of opt-out.
- kevin_thibedeau 5y agoThat's great for people weaned on Java's false promise of a uniform type system. Then you find out you want the behavior of unsigned integer overflow and have to jump through contortions to get it. You can't set a single standard for the default that works universally.
- thebruce87m 5y agoCan’t you just use the inttypes.h along with stdint fixed width types to avoid the multiple compiles? This stackoverflow answer gives an example: https://stackoverflow.com/questions/7597025/difference-between-stdint-h-and-inttypes-h https://stackoverflow.com/questions/7597025/difference-betwe...
- edflsafoiewq 5y agoOf course if you just do everything correctly you don't need safety. But if you are fallible it is nice to have.
- tinkersleep 5y agoOk, thanks for the hint. I put the most important infos into the intro: you just don't need 'll', 'l', 'z' modifiers for specifying sizeof(operand), as the compiler does that via _Generic.
- eps 5y agoAlso put an example in the first pageful. I almost lost hope while scrolling through the wall of format spec when I finally saw the first example.
- tinkersleep 5y agoOK, yes, good idea.
- 37ef_ced3 5y agoIf you want a modern C, use Go. Unless you need maximum performance (SIMD, GPUs, etc.) you should use a developer-efficient, productive language. Well-written Go executes almost as fast as C, and you will be more productive as a programmer.
- baybal2 5y agoBeware of Go. Google may use it to do the "Embrace Extend Extinguish" move. It might be type safe, but not ideologically safe. This is on top of Go being an unstable, immature language.
- 37ef_ced3 5y agoGo is very stable, and 12 years old.
- nikki93 5y agoHave you compared the performance and generated binary size of C vs. Go on WebAssembly?
- 37ef_ced3 5y agoFor WebAssembly, use the TinyGo Go compiler: https://tinygo.org/ https://tinygo.org/
- nikki93 5y agoYeah the Go -> C++ compiler I linked from my other comment is pretty much overlapping with this idea. TinyGo is still a bit early afaict and also tries to exactly implement Go semantics but I'm kind of interested in extending / adapting it to my use case.
- vladharbuz 5y agoHas anyone benchmarked Go's garbage collector lately? I like a lot of stuff about Go, but a lot of my work is in video games and real time audio, and I am extremely hesitant to use a garbage collected language for those things.
- jhallenworld 5y agoSo I also have a custom printf, but there is a limitation: if you ask gcc to check it with "__attribute__((__format__ (__printf__", then you are forced into using gcc's idea of what the printf format string syntax. How can I have strict type checking, but a user defined format string?
- kevin_thibedeau 5y agoWrite your own linter.
- wahern 5y agoThe format attribute takes the argument positions of the format string and the first variadic argument. You can pass the types as a separate array (compound literal array) as any argument before the beginning of the variadic portion, just as you would if passing in the number of variadic arguments (which one should also do to ensure the number of format specifiers matches the number of passed arguments). The magic for detecting types (e.g. _Generic) would all be the same, but there'd be a little more duplication for the variable argument macro magic. I've implemented this both ways and I don't recall there being any significant difference, but it's been awhile.
- WalterBright 5y agoD supports calling C functions directly, including printf. When we added printf format checking against the arguments, many bugs were exposed and fixed. It was a big win.
- kazinator 5y agoGCC has this also; this work is different because it removes the type errors. Instead of having an error like "%d expects a parameter of type int, not char ", it just lets %d print the string anyway. It's more like format* in Lisp, say: [1]> (format t "~05,'0d" 5) 00005 NIL [2]> (format t "~05,'0d" "abc") 00abc NIL Here ~d (decimal) doesn't care that it didn't get an integer.
- WalterBright 5y agoFor D's formatted write function, %s means "I don't care what type it is, just do the write thing with it" which works fine for nearly all uses.
- kazinator 5y agoIt's a big mistake that the format language looks like that of printf. If you use this in a big code base, there will still be the old printf all over the place. Now you have to think: is this custom logging function here based on the safe printf from github, or is it vsprintf under the hood?
- tinkersleep 5y agoYou are right, it was a poor decision. The format is not C compatible, and using the same sigil is dangerous when switching and this library's format is passed to standard printf. This started out as a drop-in replacement, but it isn't, and cannot be. The library should not encourage, but avoid accidental format string mistakes. So thank you, this is fixed.
- tinkersleep 5y agoProbably the 1e6th approach, but anyway, I also wanted to play with this myself: here's a _Generic and macro based approach to get printf type-safe in C. It needs C11, and uses some gcc extensions.
- marcodiego 5y agoI, a few times, got reasonably far implementing a generic, type-safe, variadic, macro-based and using _Generic "print" for C. I copied some examples of how to implement variadic macros, and expanded on that for C basic types. It mostly worked, you'll always have difficulty for corner cases like separating pointers and arrays, but it worked well for the basic C types. I gave up for a few reasons: - I wanted a form to register new types, so it could work for user-defined types; - the C pre-processor knows nothing about lists that can be expanded multiple times; - variadic C macros are ugly hacks. Maybe one day I'll get back to it and publish it. The interesting part is that _Generic combined with macros allows some very interesting tools for implementing primitive forms of polymorphism. Actually, if the C pre-processor supported lists, it would be possible to implement RTTI in C.
- bumblebritches5 5y ago> - I wanted a form to register new types, so it could work for user-defined types; > - the C pre-processor knows nothing about lists that can be expanded multiple times; I'm actually working on both features as Clang extensions. #repeat, a preprocessor directive to loop, can be combined with _Pragma(push_macro/pop_macro) to create lists by redefining a macro. and currently #increment, though I think I want to expand on this so that other macros can be redefined more easily to create lists via push/pop macro. The reason push_macro/pop_macro pragmas can't work, is the macro has to be undefined and redefined, and the value then pushed onto a stack in the compiler. and you can't redefine a macro in the body of another macro directly. so I've been thinking about maybe a _Pragma(redefine_macro(MacroToRedefine, NewValueForRedefinedMacro)) but I don't want it to be limited to the _Pragma area of the compiler, I want it to be eventually standardized. I've been talking to a friend at WG14 who suggested making it a "Preprocessor Expression, like `__has_c_attribute` and `defined()` So that's the area I've been working on recently for the Increment/Redefine PE lately.