💾 Archived View for nytpu.com › gemlog › 2021-07-23.gmi captured on 2023-04-19 at 23:06:35. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2022-03-01)
-=-=-=-=-=-=-
For reasons unknown to everybody including myself, I love writing C. Maybe I'm just a masochist, but I find that once you get a personal “standard library” of useful snippets built up it's pretty pleasant to write—as long as you don't try to get too clever (confusing) with your code.
Screen reader notes: all preformatted blocks on this page are of C code.
Everyone always talks about the bad things about C, which there is definitely no shortage of. And yet, no one talks about the good things. Here's some of my favorites.
I love designated initializers for structs and arrays, you can even chain them to initialize nested structs. And C is nice enough to zero all fields not explicitly initialized.
struct { char *hello; struct { bool world; int array[5]; } foo; } bar = { .hello = "world", .foo.world = true; .foo.array[0] = 0xDEADBEEF; // .foo.array[1] through .foo.array[4] are zeroed };
Fun fact: C has had designated initializers since C99 (and most compilers' extensions supported it since C90). It only took C++ twenty-one/thirty more years to get the same exact thing :P
Anonymous unions inside structs make it really easy to do tagged unions, just an enum and an anonymous union. Anonymous structs let you organize structs into logical subsections without changing the actual usage.
struct { // header struct { long dest_ip; char *checksum; } // payload struct { enum { Foo, Bar, Baz } type; union { int foo; char bar; struct { int one; int two; int three; } baz; }; }; } new_network_protocol_packet;
VLAs have a bad name, but they really aren't any worse than just doing `char array[EXCESSIVELY_LARGE_MAX_SIZE]`, you're still going to overflow the stack either way, at least VLAs are useful and logical. Multidimensional VLAs in particular are very useful so you don't have to do multiplications like you would for a regular malloc'd array. Of course, you shouldn't use a VLA if you have a known fixed maximum size, particularly if that size is small.
I think the key to avoiding a lot of C's annoying pitfalls is having a good, clear style. I guess I'll just go through my style in this section, it's pretty unusual in places but usually I have a reason behind it.
I always use strict C99 or C11 and POSIX 2001 or 2008. I never use language extensions unless I'm targeting a specific piece of hardware where the code isn't meant to be portable.
To start, I like to keep lines less than eighty characters, but it's not a hard limit and I break it if it looks better as one line.
I always use hard tabs that are set to four characters wide. Hard tabs both because I like to be able to change widths arbitrarily and I assume most other people like to customize their widths too; it reduces code size /significantly/ (every tab character saves 3 bytes); but also for accessibility reasonsᵃ, not that anyone else ever works on my code :P
[a]: thread about tabs for accessibility on reddit
I use PascalCase for types and constants, snake_case for variables and functions, and UPPERCASE_SNAKE_CASE for macros.
I wrap function calls and other long statements like this:
fprintf( stderr, "%s Error (%d): %s\n", getprogname(), errno, strerror(errno) );
I never typedef structs, enums, or unions unless I want them to be an opaque type. The extra contextual information gained from the full “struct Foo” or “enum Bar” is far worth the extra typing. If it's an opaque type not revealed to the user then typedefing is preferred though.
I always use declare pointer arguments as `const` if the function doesn't modify them.
I use language facilities over preprocessor macros when possible, so for instance I use static const strings for literals and anonymous enums for numeric constants most of the time:
static const *stringy = "hello, world!"; enum { life = 42 };
instead of
#define STRINGY "hello, world!" #define LIFE 42
I don't write side-effects in if statements or other control flow, I instead do something similar to Go-style error handling like:
int rc = open(...); if (rc < 0) { perror(errno); return -1; }
I do write side effects in for/while loop heads though.
The most unusual thing about my C style is probably how I write switch...case statments:
switch (opt) { break; case 'a': append = true; break; case 'l': loose = true; break; case 's': silent = true; break; case 'h': { usage(); exit(EXIT_SUCCESS); } break; case 'v': { printf("%s v%s\n", getprogname(), version); exit(EXIT_SUCCESS); } break; default: { usage(); exit(EXIT_FAILURE); } }
I picked up the “leading ‘break’” thing from something I read online, but I don't remember where. Even if I return or exit in the preceding statement, I still add the leading break before every case. This allows me to rearrange them arbitrarily and still have it be guaranteed that every one has a break. If I have more than one line in the case, I always use a block and always indent to clearly demarcate where that case begins and ends.
I do a sort of pseudo object-oriented layout with how I organize my projects. I keep stuff in files like `object.c` and all the definitions in that file are like `object_action()`. `object` doesn't have to be an actual struct or other type though, it could be more abstract, for example in a Gemini server, “return.c” could contain functions for sending return codes to the client. Most C files have their own header files.
C's standard library is very limited, even with POSIX, so most people I know end up with their own “library” of snippets and functions that they can copy and paste to make stuff easier. I too have this, but I haven't got a lot of it organized enough to post most of it.
I do have a C project generator that adds a lot of utility functions into the generated project
and I have collected some of my snippets here
One thing I want to get cleaned up and published is my “arena allocator.” In functions with lots of temporary allocations it makes a lot more sense to just free everything all at once after the function is done rather than trying to make sure you free every little variable on every code path. Also more efficient to just return a static buffer pointer rather than a full malloc() call too.
I found something similar to my implementation here
There's also lots of drop-in “libraries” with a single header or a single source and single header file:
https://github.com/nothings/stb
https://github.com/nothings/single_file_libs
https://github.com/clibs/clib/wiki/Packages
⁂