8 ms·
Lesser known tricks, quirks and features of C
- deleted 2y ago[deleted]
- coreyp_1 2y agoThat's a nice list! I've been digging into cross-platform (Windows and Linux) C for a while, and it has been fascinating. On top of that, I've been writing a JIT-ted scripting (templating) language, and the ABI differences (not just fastcall vs stdcall vs cdecl) are often not easy to find documentation about. I've decided that if I ever get to teach a University class on C again, I wanted to cover some of these things that I feel are often left out, and this list is a helpful reference! Thanks!
- rramadass 2y agoThis is actually a pretty good list and that's why i submitted it to HN. The Chinese stratagem "Cast a Brick to attract Jade" is relevant here though i haven't yet seen much "Jade" from others :-) The author's presentation/explanation is also quite succinct and precise with references pointing to further details and thus the overall s/n ratio is very good. This is how tech stuff should be written (contrast with meandering articles with one technique being "explained" over five pages). Knowing these sort of techniques is important because they force you to think in different ways to solve a problem which expands one's mental design space. C (and C++) is particularly important here since it is the common "lingua-franca" across all system/application software from servers to desktops to itty-bitty MCUs. PS: Also see the book Fluent C by Christopher Preschern which while not dealing with "tricks" shows how to use C effectively using a pattern-like approach.
- ranger_danger 2y ago> quirks and features Someone is a fan of Doug DeMuro.
- randomdata 2y agoThis... is the 1972 Riche C
- saagarjha 2y agoMentioning %n without explaining that it is overwhelmingly used for exploits is a little reckless IMO.
- greiskul 2y agoI'm curious about this, didn't know about %n before. What are the common pitfalls and exploits using this enables?
- lights0123 2y agoIf the user can control the formatting string, they can write to pointers stored on the stack. It's important to use printf("%s", str) instead of printf(str).
- mananaysiempre 2y agoYou would expect a printf call with a user-controlled format string to be, at worst, an arbitrary read. Thanks to %n, it can be a write as well.
- _kst_ 2y agoBackground: A %n format specifier in a printf call stores the number of characters written so far into a specified variable. For example: #include <stdio.h> int main(void) { int count; printf("%s%n\n", "hello, world", &count); printf("count = %d\n", count); } The output is: hello, world count = 12 %n can be exploited to write data to an arbitrary memory location, but only if the format string is something other than a string literal. %n can be exploited, but it's entirely possible to use it safely.
- jonathrg 2y agoMulti character constants is one of the many things in C that would be nice to use if the language would just choose some well-defined behaviour for it. It doesn't really matter which.
- mananaysiempre 2y agoMainstream compilers agree on multicharacter literals being big endian; that is, 'AB' is usually 'A' << CHAR_BIT | 'B'. The exception is MSVC, which also works like that as long as you don't use character escapes, but if you do it emits some sort of illogical, undocumented mess that looks like an ancient implementation bug fossilized into a compatibility constraint.
- poincaredisk 2y agoYour phrasing is a bit confusing. Multicharacter litersl being big endian (as you've defined it) means that it'll actually end up little-endian in memory (in little endian architectures, like x86). So 'ABCD' will end up as 'DCBA' in memory. Really interesting to hear about the escaping quirk, I need to test it.
- golergka 2y agoswitch (n % 2) { case 0: do { ++i; case 1: ++i; } while (--n > 0); } Someone is really ought to record a "WAT" video about C.
- lifthrasiir 2y agoJust turn every single IOCCC winning entry into a video, you now have a year's supply of contents.
- mananaysiempre 2y agoThe switch statement in C is not a very limited pattern match. The switch statement in C is a very ergonomic jump table. Do not think ML’s case-of with only integer literals for patterns; think FORTRAN’s computed GO TO with better syntax. And it will cease to be a WAT. (For a glimpse of the culture before pattern matching was in programmers’ collective consciousness, try the series on designing a CASE statement for Forth that ran for several issues of Forth Dimensions.)
- russellbeattie 2y agoI don't think there's any confusion of how it works, it's the deep horror in discovering that it's possible in the first place, and a morbid curiosity of the chaos it could cause if abused.
- mananaysiempre 2y agoAt least for me, the feelings you describe are characteristic of a footgun, not a WAT. A WAT is rather a desperate bewilderment as to who could ever design the thing that way and why, and for switch statements computed gotos are the answer to that question. As for the footgun issue, I mean, it could be one in theory, sure. But I don’t think I’ve ever seen it actually fired. And I can’t really appreciate the Javaesque “abuse” thinking—it is to some extent the job of the language designer to prevent the programmer from accidentally doing something bad, but I don’t see how it is their job to prevent a programmer from deliberately doing strange things, as long as the result looks appropriately strange as well. (There are reasons to dislike C’s switch statement, I just don’t think the potential for “abuse” is one.)
- fuhsnn 2y agoMy recent favorite is glibc's hack to implement _Static_assert under C99: https://codebrowser.dev/glibc/glibc/misc/sys/cdefs.h.html#564 https://codebrowser.dev/glibc/glibc/misc/sys/cdefs.h.html#56... It uses the constant expression to create a bitfield of size -1 when failed, and leaves the compiler to error on that as the intended assertion. The actual statement is an extern pointer to a function returning a pointer to an array which has sizeof the aforementioned bitfield struct as its size. Another one encountered in Toybox is (0 || "foo") being a const expression that evaluates to 1. Apparently the string literal must have been soundly created in data section, so its pointer address is safely assumed to be non-zero.
- lifthrasiir 2y agoYou have missed one important thing: every passing assertion will define a single extern function pointer with the same signature, so multiple `_Static_assert` invocations can coexist in a single scope. An extern definition doesn't have to be a function pointer by the way, I guess it helped a linker to have an easier time when removing unused symbols.
- fuhsnn 2y agoOops too late to edit, that's really a function prototype. So it wouldn't take storage space or affect symbol unless the user naughtily calls the __Static_assert_function.
- wolfspaw 2y agoReally liked the trick of defining the struct in the return part of the function. Array pointers: Array to pointer decay is extremely annoying, if it was implemented as Array to "slice" decay it would be great. Static array indices in function parameter declarations: awesome, a shame that C++ (and Tiny C) do not support it >/ flexible array member: extremely useful, and now there are good compiler flags for ensuring correct flexible array member usage X-Macro: nice, no-overhead enum to string name. Didn't know the trick Combining default, named and positional arguments: Named-arguments/default-arg, C version xD. It would be cool if it was added to C language as a native feature, instead of having to do the struct hiding macro. Comma operator: really useful, specially in macros Digraphs, trigraphs and alternative tokens: di/tri/graphs rarely useful, alternatives synonims of iso646.h are awesome, love using and/or instead of &&/|| Designated initializer: super awesome, could not use if you wanted C++ portability. Now C++ supports some part of it. Compound literals: fantastic, but in C++ it will explode due to stack deallocation in the same line. C++ should fix this and allow the C idiom >/ Bit fields: nice for more control of structs layout constant string concat: "MultiLine" String, C version xD Ad hoc struct declaration in the return type of a function: didn't know this trick, "multi value" return, C version xD Cosmopolitan-libc: incredible project. Already knew of it, its awesome to offer a binary that runs in all S.Os at the same time. Evaluate sizeof at compile time by causing duplicate case error: ha, nice trick for debugging the size of anything.
- fuhsnn 2y ago>Static array indices in function parameter declarations: awesome, a shame that C++ (and Tiny C) do not support it >/ The first array size is actually always decayed to a pointer, supporting it in a compiler without analysis passes like TCC is just a matter of skipping the "static" token and the size.
- WalterBright 2y ago> Array to pointer decay is extremely annoying, if it was implemented as Array to "slice" decay it would be great. It's not just annoying, it's the major source of bugs in shipped code. A fix: https://www.digitalmars.com/articles/C-biggest-mistake.html https://www.digitalmars.com/articles/C-biggest-mistake.html
- o11c 2y agoBah, those are all well-known. What value does the following program return? int main() { int *p = 0; loop: if (p) return *p; int v = 1; p = &v; v = 2; goto loop; return 3; } Also, rather than doing `sizeof` via one error at a time, it's better to just emit them to a char array {'0' + sz/10, '0' + sz%10, '\0'}. Generalizing this to signed numbers of arbitrary size is left as an exercise for the reader.
- sweeter 2y agoIs it 2? I'm not exactly sure though. I'm interested in hearing the logic
- _kst_ 2y agoIt returns 2. The only reason that might be surprising is that the "return *p;" statement refers to the value of an object at a point (textually) before its definition. But the lifetime of the object named "v" begins on entry to the innermost compound statement enclosing its definition -- in this case the body of "main". Space for "v" is allocated on entry to "main". It's initialized to 1 when its definition is reached. The "return *p;" statement appears before the definition of "v" in the program source, but is executed after its definition was reached at run time, and within its lifetime. It's important to remember that scope and lifetime are two different things. The scope of an identifier is the region of program text in which the identifier is visible; for "v" it extends from the definition to the closing "}". The lifetime of an object is the time span during execution in which it exists; for "v" it extends from the time when execution reaches the opening "{" to the time when execution reaches the closing "}". Formally, storage for "n" is allocated at the beginning of its lifetime and deallocated at the end of its lifetime. Compilers can and do optimize allocation and deallocation, as long as the visible behavior is consistent. Aside: If "v" were a VLA (variable length array, introduced in C99, made optional in C11) its lifetime would begin when execution reaches its definition.
- johnklos 2y agoNot sure what happened: 404 File not found The site configured at this address does not contain the requested file.
- ericpruitt 2y agoI'm getting the same error. This appears to be the article in question: https://github.com/Jorenar/blog/blob/gh-pages/_posts/2023-02-19-less-known-c.md https://github.com/Jorenar/blog/blob/gh-pages/_posts/2023-02...
- lifthrasiir 2y agoI hate I know all of them... > Backslash line splicing One reason a trigraph was gone is that `??/`, a trigraph spelling for `\`, also acted like `\` in this context. > Using `&&` and `||` as conditionals Not only this is uncommon, but chaining them is not always correct because `a && b || c` doesn't equal to `a ? b : c` when `b` evaluates to false. > Compile time assumption checking using `enum`s Please use `static_assert` already. > Matching character classes with `sscanf()` This can be combined with `*` to ignore certain characters. For example `%*[ \t]` will skip all horizontal whitespaces, unlike a plain whitespace which will also skip newlines. > Detecting constant expressions This ultimately comes from C's weird way to say a null pointer, which is defined as any constant expression which type is inferred to be pointer. So a non-constant expression can be distinguished by multiplying it with a known zero constant.
- fsckboy 2y ago> `??/`, a trigraph spelling for `\`, also acted like `\` in this context. OF COURSE it should do what \ does, otherwise you have no other way to get a \ the point of trigraphs is to allow characters to be entered that your character-set/terminal keyboard doesn't allow.
- lifthrasiir 2y agoThat's technically true, but it could have been designed much better if that was the real intention: 1. There is no real reason that trigraphs should be expanded inside a comment. Preprocessors can't make any additional comment, so the comment should be scanned and discarded as soon as possible but trigraphs somehow precede that. 2. And this very behavior of backslash should have been also deferred as much as possible. ISO C already has two sets of doubly-quoted literals `"asdf"`, where one is used for normal string literals and another is used for preprocessing because `#include "foo\bar.c"` should refer to the file name that contains a backslash, not a backspace (`\b`). Since `#include FILENAME` is also possible, such literals may appear anywhere in the preprocessing line! Therefore we already have to defer processing of some backslashes, so why should remaining backslashes be processed that early? In my ideal design, a backslash is either a part of tokens (`+\<newline>=`, `foo\<newline>bar` or `"asdf\n\<newline>fdsa"`) or a standalone token optionally followed with a newline (`\<newline>`). No backslashes within comments are significant, effectively solving the first point. These tokens are then turned into normal tokens or whitespaces respectively, so they remain transparent to the parser. The trigraph could then have been allowed to replace backslashes in such cases (e.g. `+??/<newline>=`) without affecting remaining cases like comments. For the record, later digraphs are more or less designed as such but they lack backslashes, even though ISO/IEC 646 still doesn't contain backslashes for all charsets. This hints that the inclusion of trigraphs or digraphs was more due to vendor complaints (known to be IBM) than actual concerns from users who wouldn't be able to type backslashes if it were true.
- 38 2y ago> int (*ap1)[10] = &arr; Wow that's garbage syntax. With Go it would be var ap1 *[10]int = &arr
- pavlov 2y agoMaybe you missed the part where this is C, you know, the language designed by many of the same people as Go but 35 years earlier. It would be a time warp worthy of the Rocky Horror Picture Show if C's design could take syntax ideas from Go.
- rramadass 2y agoI actually find the C syntax easier to read and understand.
- lifthrasiir 2y agoHow about the following then? I can read them but by no means they are intuitive. int *x[10]; // How is this different from `ap1` above? int (*f (void))[10];
- rramadass 2y agoThese are all simple (not necessarily intuitive) if you know how operator binding works in C, (using braces to highlight); int *x[10]; ---> { int* } x[10]; int (*f (void))[10] ---> int { { (* {f (void) } ) } [10]; } The point is that once you have had some practice you can work it out and Go's syntax is not necessarily much better.
- lifthrasiir 2y agoYour original comment said it "is easier to read and understand", not "can be worked out after some practice". Of course it is not like you should inline every `typedef`s into a single mess of complicated types, but you never said that you believe only simplest types should be used and C syntax is easier for them. In any case, I think Go is a clear winner here because all logical types are consecutive tokens. For example `f` in my example is (as you correctly parsed) a pointer to a function that returns an array of 10 integers, but that return type is normally written `int [10]` or `int NAME[10]`, while here is written in two chunks `int` and `[10]` with a big parenthesis inside.
- guerrilla 2y agoThese are great. Most posts I read with titles similar to this are just the authors revealing that they don't know C very well but this one included some interesting things. I didn't know compund literals were lvalues but if you think about executable formats, it makes a lot of sense.
- rramadass 2y agoThe references linked to are also a pretty good source of similar info.
- winocm 2y agoThere’s also the use of typedef to help make function declarations. Such as: typedef void fptr_t(int); fptr_t foo; That would effectively declare a function with the prototype: `void foo(int)'. This pattern is used quite a bit in BSD kernels.