Ask HN: Good practices for my first C project?
51 comments
* +1 to Valgrind.
* +1 to sanitizers.
* +1 to clangd and bear.
* +1 to namespacing.
* Make sure you have a code formatter. I use clang-format, but any should work.
* Do not use recent C features unless you need them. VLA's in particular are a bad choice. An example of something you might need is `alignof()` and `max_align_t`, which are basically necessary if writing your own allocator(s).
* Write destructors for your types. If they own data, their destructor should free that data. This has helped me avoid countless memory leaks.
* Finally (and this will probably be controversial), use unsigned arithmetic as much as you can! The reason for this is because unsigned arithmetic has far fewer cases of UB, and with a little work, you can simulate two's-complement signed arithmetic in C without UB.
Source: I am a C programmer with a public project that has had very few memory bugs or UB bugs found in releases. In particular, unsigned arithmetic is what got me farther than most, since most will use Valgrind/sanitizers.
* +1 to sanitizers.
* +1 to clangd and bear.
* +1 to namespacing.
* Make sure you have a code formatter. I use clang-format, but any should work.
* Do not use recent C features unless you need them. VLA's in particular are a bad choice. An example of something you might need is `alignof()` and `max_align_t`, which are basically necessary if writing your own allocator(s).
* Write destructors for your types. If they own data, their destructor should free that data. This has helped me avoid countless memory leaks.
* Finally (and this will probably be controversial), use unsigned arithmetic as much as you can! The reason for this is because unsigned arithmetic has far fewer cases of UB, and with a little work, you can simulate two's-complement signed arithmetic in C without UB.
Source: I am a C programmer with a public project that has had very few memory bugs or UB bugs found in releases. In particular, unsigned arithmetic is what got me farther than most, since most will use Valgrind/sanitizers.
> * Finally (and this will probably be controversial), use unsigned arithmetic as much as you can! The reason for this is because unsigned arithmetic has far fewer cases of UB, and with a little work, you can simulate two's-complement signed arithmetic in C without UB.
I would recommend against this. You're trading off one form of incorrect behavior (UB) for another form of incorrect behavior (wraparound on overflow is incorrect >99% of the time), with the added potential for mistakes in the emulation step.
Instead, do this:
* If you consider that wraparound on overflow is in fact the correct and desired behavior, use unsigned arithmetic.
* If you want to check for overflowing conditions, use your compiler's builtins for checked operations (C23 finally adds ckd_* ops, but it's too new to recommend use of yet).
I would recommend against this. You're trading off one form of incorrect behavior (UB) for another form of incorrect behavior (wraparound on overflow is incorrect >99% of the time), with the added potential for mistakes in the emulation step.
Instead, do this:
* If you consider that wraparound on overflow is in fact the correct and desired behavior, use unsigned arithmetic.
* If you want to check for overflowing conditions, use your compiler's builtins for checked operations (C23 finally adds ckd_* ops, but it's too new to recommend use of yet).
One of the reasons I recommend unsigned arithmetic is because you can make checked arithmetic from it. Doing it with signed arithmetic is a fool's errand.
I have, in fact, done this.
I have, in fact, done this.
It is best not to try to roll your own checked arithmetic. Your compiler already has an implementation, that will likely compile down to "check the overflow flag in hardware", and carries 0 risk of undefined behavior. Why not use it?
("It's not portable" is a reason that will cease to be true shortly--as mentioned, C23 added checked overflow functions.)
("It's not portable" is a reason that will cease to be true shortly--as mentioned, C23 added checked overflow functions.)
> ("It's not portable" is a reason that will cease to be true shortly--as mentioned, C23 added checked overflow functions.)
So in practice, today, it's not portable and won't be for a while (even when it's released into compilers, it'll take time for those new versions to trickle into the ecosystem), which is a compelling argument against using it.
So in practice, today, it's not portable and won't be for a while (even when it's released into compilers, it'll take time for those new versions to trickle into the ecosystem), which is a compelling argument against using it.
Because I want to stay on C11.
> Your compiler already has an implementation
That's a big assumption that is not true on all compilers I want to target.
> Your compiler already has an implementation
That's a big assumption that is not true on all compilers I want to target.
For a new project, make/bear are really not not the best any more. Something that outputs ninja would be much better. Faster, supports compile_commands.json out of the box, profiling support, etc.
One thing I'd add to the list: stdint.h. Don't use int, long, etc. Just use uint32_t, uint64_t and friends. Completely eliminates the ambiguity and you don't have to remember which is which.
One thing I'd add to the list: stdint.h. Don't use int, long, etc. Just use uint32_t, uint64_t and friends. Completely eliminates the ambiguity and you don't have to remember which is which.
> Do not use recent C features unless you need them. VLA's in particular are a bad choice.
VLA are closer to a deprecated feature than a recent feature: they were mandatory in C99, and made optional in C11.
VLA are closer to a deprecated feature than a recent feature: they were mandatory in C99, and made optional in C11.
I don't like bear. If you want to update the compilation database with it, you have to do a clean build. Most build systems can generate the database without a clean build, and most compilers can generate it, so I don't see much use in bear, personally.
how? cmake can do it, so does compiledb, but that's about it in addition to bear?
some makefile based build codebase can not build compile_commands.json at all, I had to use the old ctags way to jump around, as clangd could not understand the code(e.g. it can find header definitions, but could not find function implementations in other non-header source files)
some makefile based build codebase can not build compile_commands.json at all, I had to use the old ctags way to jump around, as clangd could not understand the code(e.g. it can find header definitions, but could not find function implementations in other non-header source files)
Clang itself can do it, as can forks of Clang, so can Xmake, Meson, and Ninja, as well as Clang Power Tools for Visual Studio.
clangd can barely do it in practice, it does not index the whole code base, instead it builds up the index gradually, and it often failed to find function implementation, it can just find those definitions, ctags has no issues to find both.
I would love to stick with clangd if it can do whatever ctags can as far as source indexing goes, it seems can not.
I would love to stick with clangd if it can do whatever ctags can as far as source indexing goes, it seems can not.
Clangd consumes the compilation database, it doesn't produce it. Clang is what does that.
Fair enough.
Yeah, I really don't understand why people use signed integers so often. They seem... quirky. Especially in C where signed overflow is UB. I guess they're syntactically the default.
Would you consider the fixed width integer types (uint8_t, etc) a recent C feature worth using? The older type's size being implementation defined seems like a major pain to me, but I haven't used C much.
Would you consider the fixed width integer types (uint8_t, etc) a recent C feature worth using? The older type's size being implementation defined seems like a major pain to me, but I haven't used C much.
Yes, fixed-width integer sizes are absolutely worth using.
stdint.h defining the fixed width integer is part of C99. I wouldn't call that recent !
https://pubs.opengroup.org/onlinepubs/009695399/basedefs/std...
https://pubs.opengroup.org/onlinepubs/009695399/basedefs/std...
You need to understand the timescales of C.
C89 (aka ANSI C) was "standard" C for years. C99 was released, and largely ignored until the mid-2000's, and then gradually began to grow. As of about 2015, C99 was probably the "standard" C dialect, but there is still a lot of C89 out there, especially where you have niche toolchains.
I wouldn't seriously consider C11 unless you're certain that the toolchain supports it, and there's some feature that you really need. And that's pretty unlikely.
C89 (aka ANSI C) was "standard" C for years. C99 was released, and largely ignored until the mid-2000's, and then gradually began to grow. As of about 2015, C99 was probably the "standard" C dialect, but there is still a lot of C89 out there, especially where you have niche toolchains.
I wouldn't seriously consider C11 unless you're certain that the toolchain supports it, and there's some feature that you really need. And that's pretty unlikely.
I would also recommend CodeChecker (static analysis tool - open source) or if you end up having to use gcc use gcc's -fanalyzer option for static analysis.
I find that gcc is a version or two behind clang for new compiler diagnostics and static analysis features (purely anecdotal so please take it with pinch of salt).
I find that gcc is a version or two behind clang for new compiler diagnostics and static analysis features (purely anecdotal so please take it with pinch of salt).
can you elaborate on 'namespacing' in C?
also, which way you use to do 'destructors' in C?
also, which way you use to do 'destructors' in C?
Namespacing was mentioned by another comment, but I do something like this:
For destructors, I want them to be passed and stored as first-class entities, so I defined them like this:
Works like a charm.
Edit: There is one danger to the destructors: you can pass the wrong type of pointer. But if this happens, you'll probably get a lot of Valgrind errors and a spectacular crash.
libname_module_function();
So for my bc [1], I do this: bc_parse_if(...);
That parses an `if` statement.For destructors, I want them to be passed and stored as first-class entities, so I defined them like this:
typedef void (*BcDestructor)(void* item);
Then things like my resizable arrays store the destructor for the items in the array, and when items are removed (popped, whatever), the destructor is run on the item if the destructor is not NULL.Works like a charm.
Edit: There is one danger to the destructors: you can pass the wrong type of pointer. But if this happens, you'll probably get a lot of Valgrind errors and a spectacular crash.
"namespacing" in C means prefixes on public symbols.
Typically, they're two-part. First you have the project/company prefix, and then the ADT (abstract data type) prefix. So, eg. "foo_hash_" is a prefix from company/project "foo" and is a part of the "hash" ADT interface.
As a consequence, C people like abbreviations. Which is fine in moderation.
Private functions don't need the prefixes, of course.
Public variables are a little problematic if you ever port to Windows (DLLs), so try to avoid them.
Typically, they're two-part. First you have the project/company prefix, and then the ADT (abstract data type) prefix. So, eg. "foo_hash_" is a prefix from company/project "foo" and is a part of the "hash" ADT interface.
As a consequence, C people like abbreviations. Which is fine in moderation.
Private functions don't need the prefixes, of course.
Public variables are a little problematic if you ever port to Windows (DLLs), so try to avoid them.
It's often best not to think too much about "aesthetic", or performance, at first, and to focus instead on getting something that works. FWIW, The Mythical Man-Month[0] recommends to start with a few throw-away prototypes, during which you're gaining expertise over the problem, that you can later crystallize in more definite versions.
Now, it doesn't mean good practices should be discarded altogether either. One of the most generic advice is to be systematic/coherent: if you use one convention in some place, use it everywhere. Then, reading good codebases (e.g. OpenBSD[1], Plan9[2]) is a great way to learn about organizing code, existing conventions, idiomatic techniques, etc.
Finally, regarding tools, you may want to check periodically what Valgrind[3] has to say on your code. Eventually use a Makefile or an elementary shell script to automate the build: both should be sufficient at least for the first "draft".
[0]: https://en.wikipedia.org/wiki/The_Mythical_Man-Month#The_pil...
[1]: https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/
[2]: https://9p.io/sources/plan9/sys/src/
[3]: https://valgrind.org/
Now, it doesn't mean good practices should be discarded altogether either. One of the most generic advice is to be systematic/coherent: if you use one convention in some place, use it everywhere. Then, reading good codebases (e.g. OpenBSD[1], Plan9[2]) is a great way to learn about organizing code, existing conventions, idiomatic techniques, etc.
Finally, regarding tools, you may want to check periodically what Valgrind[3] has to say on your code. Eventually use a Makefile or an elementary shell script to automate the build: both should be sufficient at least for the first "draft".
[0]: https://en.wikipedia.org/wiki/The_Mythical_Man-Month#The_pil...
[1]: https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/
[2]: https://9p.io/sources/plan9/sys/src/
[3]: https://valgrind.org/
I think one of the interesting things about C is that if you ask 100 C devs the “right way” you’ll have 100 ways! :-)
C allows you tons of freedom to make programs your own. As you also use libraries you’ll see various approaches and conventions. For example SDL use SDL_FuntionName() while something like Cairo uses cairo_function_name()
I totally hear you because those same questions about “best practices” were the ones I had when I started C. C is and incredibly powerful tool that lets you almost anything! So, just enjoy the ride and let your taste develop as you go!
I personally use all standard ints so uint8_t instead of unsigned char. I also like namespacing stuff, like the functions in my_library.h would have something like ml_function_one() and ml_function_two().
I’ve also come to appreciate immensely macros! Sure, they can be as dangerous and as useful as a very sharp Japanese Damascus steel knife in the kitchen, use your discretion ;-)
But above all, have fun! C puts almost no limits on you and soon you’ll find yourself passing function pointers, making system calls and playing with memory as if it was nothing!
Have fun!
C allows you tons of freedom to make programs your own. As you also use libraries you’ll see various approaches and conventions. For example SDL use SDL_FuntionName() while something like Cairo uses cairo_function_name()
I totally hear you because those same questions about “best practices” were the ones I had when I started C. C is and incredibly powerful tool that lets you almost anything! So, just enjoy the ride and let your taste develop as you go!
I personally use all standard ints so uint8_t instead of unsigned char. I also like namespacing stuff, like the functions in my_library.h would have something like ml_function_one() and ml_function_two().
I’ve also come to appreciate immensely macros! Sure, they can be as dangerous and as useful as a very sharp Japanese Damascus steel knife in the kitchen, use your discretion ;-)
But above all, have fun! C puts almost no limits on you and soon you’ll find yourself passing function pointers, making system calls and playing with memory as if it was nothing!
Have fun!
1. initialize all your variables - you will trip over this one, and it's always nasty
2. I typically throw a _ptr at then end for global pointers
3. use a unity build - create a build.c file and directly #include all .c files, and then just compile your build.c file. This will speed up compilation by many orders of magnitude, as well as allow the compiler to make better optimizations. Don't think too hard about how to separate your C files. I typically start in one file and I begin separating as themes or modules start to emerge. And even then, I wait a while, because I may change my mind. This allows my designs to become quite refined.
4. Make as few allocations as possible.
5. no comment
6. I get a personal kick out of sticking to C89, to my own detriment probably.
7. Errors are handled differently on a per function basis. But when an error requires a lot of cleanup, don't be afraid to use a GOTO.
8. Throw -fsanitize=undefined in there as well.
2. I typically throw a _ptr at then end for global pointers
3. use a unity build - create a build.c file and directly #include all .c files, and then just compile your build.c file. This will speed up compilation by many orders of magnitude, as well as allow the compiler to make better optimizations. Don't think too hard about how to separate your C files. I typically start in one file and I begin separating as themes or modules start to emerge. And even then, I wait a while, because I may change my mind. This allows my designs to become quite refined.
4. Make as few allocations as possible.
5. no comment
6. I get a personal kick out of sticking to C89, to my own detriment probably.
7. Errors are handled differently on a per function basis. But when an error requires a lot of cleanup, don't be afraid to use a GOTO.
8. Throw -fsanitize=undefined in there as well.
Initializing variables means the compiler will not be able to warn you when you use ununitialized value. I prefer to get the warning than use zero initialized one by accident.
Build.c file - good idea if performance is priority but you remove an option to get incremental builds (with make or the like).
I would add: read about warning options and enable as many as sensible ("-Wall -Wextra -pedantic from memory but there are a few others). Treat warnings seriously, don't allow them for longer than debugging/experimenting stage.
C99 has some useful features (like designated initializers or compound literals). Check if your compiler supports it.
Build.c file - good idea if performance is priority but you remove an option to get incremental builds (with make or the like).
I would add: read about warning options and enable as many as sensible ("-Wall -Wextra -pedantic from memory but there are a few others). Treat warnings seriously, don't allow them for longer than debugging/experimenting stage.
C99 has some useful features (like designated initializers or compound literals). Check if your compiler supports it.
There's nothing wrong with sticking to C89, but honestly, stdint.h is hard to pass up. That said, you can define your own sized integers using the stdint names in about a few dozen lines, since you don't really want to be using most of the stuff in the standard version.
oh, I love the idea of making a build.c!
Specially because I came across a compiler (sdcc) that doesn't support multiple source files (everything should be compiled individually and then linked with main.c
Specially because I came across a compiler (sdcc) that doesn't support multiple source files (everything should be compiled individually and then linked with main.c
cURL is one of the most used C libs and is an example of good quality C code. If you follow the style used there, see e.g. https://github.com/curl/curl/blob/master/lib/dynhds.h (and associated dynhds.c) you will be good.
Looking at the source of some of the old game-engines from the era that have since been released as open-source can also be helpful, like https://github.com/id-Software/DOOM.
In both cases notice how simple and elegant a lot of the code is. There is already enough complexity inherent in the problem they are solving, and that is where the focus should be.
Any IDE with a working language server to make it easy to jump around and refactor should work fine. Limitations might be due to the C language itself?
Error handling on such a fixed platform does not need to be super-advanced. You should always be within the confines of the system so there shouldn't be much that can go wrong. If stuff goes wrong anyway just being able call a function Fatal("FooBar failed with code 34") when unexpected stuff happens and have it log somewhere to be able to dig around should be enough. You never need to be able to recover and retry.
Make sure to use https://clang.llvm.org/docs/AddressSanitizer.html or a similar tool when developing outside of the PSOne.
That said, consider statically allocating global buffers for most stuff and avoid using the heap as much as possible. This will give you deterministic memory usage, no overhead, and simple code.
Good luck working within the confines of the PSOne! Many hackers have pulled the hair from their head on that platform ;)
Looking at the source of some of the old game-engines from the era that have since been released as open-source can also be helpful, like https://github.com/id-Software/DOOM.
In both cases notice how simple and elegant a lot of the code is. There is already enough complexity inherent in the problem they are solving, and that is where the focus should be.
Any IDE with a working language server to make it easy to jump around and refactor should work fine. Limitations might be due to the C language itself?
Error handling on such a fixed platform does not need to be super-advanced. You should always be within the confines of the system so there shouldn't be much that can go wrong. If stuff goes wrong anyway just being able call a function Fatal("FooBar failed with code 34") when unexpected stuff happens and have it log somewhere to be able to dig around should be enough. You never need to be able to recover and retry.
Make sure to use https://clang.llvm.org/docs/AddressSanitizer.html or a similar tool when developing outside of the PSOne.
That said, consider statically allocating global buffers for most stuff and avoid using the heap as much as possible. This will give you deterministic memory usage, no overhead, and simple code.
Good luck working within the confines of the PSOne! Many hackers have pulled the hair from their head on that platform ;)
I'm not seeing much advice related to the target itself (PlayStation 1). That target has both embedded constraints (2 MiB of RAM, no MMU...) and platform-specific pitfalls that you don't see when targeting a modern system.
Off the top of my head:
- The PlayStation CPU doesn't have a FPU, forget about using floats.
- You only have 2 MiB of RAM and no MMU. Avoid malloc(), memory leaks and especially heap fragmentation will be fatal ; prefer custom allocation strategies that leverages lifetime properties of your data structures (static, bump, arena, per-frame...).
- The original PlayStation SDK is very, very old. Consider using something more modern like PSn00bSDK.
- Consider using modern emulators with debugging facilities like DuckStation for most of the development, they're fairly accurate nowadays and you'll have a much better debugging and iteration experience than relying on bare metal.
Now, I'm assuming your game will be fairly lightweight performance-wise. If you're going for something even moderately ambitious you'll quickly run into bottlenecks and limitations that will force you to properly understand your target and how to design your game around it. I'm not actually a PlayStation programmer (although I am reverse-engineering a PlayStation video game) so I can't give proper target-specific on how to use the scratchpad, the GTE and so on.
Off the top of my head:
- The PlayStation CPU doesn't have a FPU, forget about using floats.
- You only have 2 MiB of RAM and no MMU. Avoid malloc(), memory leaks and especially heap fragmentation will be fatal ; prefer custom allocation strategies that leverages lifetime properties of your data structures (static, bump, arena, per-frame...).
- The original PlayStation SDK is very, very old. Consider using something more modern like PSn00bSDK.
- Consider using modern emulators with debugging facilities like DuckStation for most of the development, they're fairly accurate nowadays and you'll have a much better debugging and iteration experience than relying on bare metal.
Now, I'm assuming your game will be fairly lightweight performance-wise. If you're going for something even moderately ambitious you'll quickly run into bottlenecks and limitations that will force you to properly understand your target and how to design your game around it. I'm not actually a PlayStation programmer (although I am reverse-engineering a PlayStation video game) so I can't give proper target-specific on how to use the scratchpad, the GTE and so on.
The main footman in C for novices is memory handling.
-Separate all memory creation and destruction(anything that has a malloc and free on it) and handling of it into independent modules(files).
-Automate the above memory creation and destruction with a language like CLisp or Clojure so everything that access memory uses the same code that is well tested and minimal. Creating primitive equivalents to C++ vectors or node handling is extremely useful.
Wit automation you can warrantee that everything allocated is freed, but if you are not used to meta programming you can start doing it manually.
-Always use limited arrays for accessing things and structures, use single digits globals in your entire program. The complex pointers' arithmetic should be limited to the memory isolated modules and only should be done if you really know what you are doing.
-Never be too "smart", write simple code. Do not use more that "pointers to pointers" complexity(forbidding things like pointers to pointers to pointers). Create multiple simple lines instead of long sophisticated ones.
-Use Valgrind, specially in the isolated modules and always make it to have 0 warnings.
-If you use Xcode, learn to use the debugger(lldb) for sophisticated debugging(learn about breakpoints, watchdogs and printing variables). If you learn the text commands you will be able to use it anywhere(command line, vim or emacs), in any OS(windows, linux, Mac, WebAssembly).
-Separate all memory creation and destruction(anything that has a malloc and free on it) and handling of it into independent modules(files).
-Automate the above memory creation and destruction with a language like CLisp or Clojure so everything that access memory uses the same code that is well tested and minimal. Creating primitive equivalents to C++ vectors or node handling is extremely useful.
Wit automation you can warrantee that everything allocated is freed, but if you are not used to meta programming you can start doing it manually.
-Always use limited arrays for accessing things and structures, use single digits globals in your entire program. The complex pointers' arithmetic should be limited to the memory isolated modules and only should be done if you really know what you are doing.
-Never be too "smart", write simple code. Do not use more that "pointers to pointers" complexity(forbidding things like pointers to pointers to pointers). Create multiple simple lines instead of long sophisticated ones.
-Use Valgrind, specially in the isolated modules and always make it to have 0 warnings.
-If you use Xcode, learn to use the debugger(lldb) for sophisticated debugging(learn about breakpoints, watchdogs and printing variables). If you learn the text commands you will be able to use it anywhere(command line, vim or emacs), in any OS(windows, linux, Mac, WebAssembly).
Beginners tend to get way too consumed by granular resource allocation and freeing. Often writing lots of verbose nonsense because the language/standard library don't come with abundant abstractions out of the box to obviate the need for such things. Learn to build those abstractions when you find yourself repeatedly doing a bunch of tedious rigamarole-y resource management code, diy trivial allocators are very common in games for a reason. e.g. Allocate in a leaky manner by encapsulating those allocations in an allocator instance you can throw away at the end of a procedure, or at the end of every frame, or end of a level, etc.
Since the goal is a vintage console like PSOne I'd probably stick with C89 at most, unless you know the toolchains you'll be using are modernized.
Also when it comes to video games, unless you're writing something like an MMO server side component, robust error handling w/recovery is generally a tremendous waste of time and code. Use assert() and use it often. e.g. Don't try to handle and recover from a malloc() failure, just assert malloc() doesn't return NULL in your wrapper. It's a video game, not Apache.
Since the goal is a vintage console like PSOne I'd probably stick with C89 at most, unless you know the toolchains you'll be using are modernized.
Also when it comes to video games, unless you're writing something like an MMO server side component, robust error handling w/recovery is generally a tremendous waste of time and code. Use assert() and use it often. e.g. Don't try to handle and recover from a malloc() failure, just assert malloc() doesn't return NULL in your wrapper. It's a video game, not Apache.
Like others have said, lean on your experience in other languages and what comes naturally through intuition with regards to tools, naming conventions and the like. What you can't always reason about from first principles, you could see examples in use by reading more code.
Since you're looking at C, I would highly suggest reading through and getting familiar with and taking inspiration from successful C language projects: sqlite, cURL, redis, memcached, haproxy and others (and perhaps the linux kernel too, but that may be skirting the line of too low-level). Granted these are closer to 'systems' level code than higher level app code, but from my experience they have a lot of translatable goodies. They all have some good idiomatic, readable, intuitive C that you can learn a bunch of good practices from. Look at how those use makefiles and build processes, possibly linters too, to their advantage and shamelessly take the good parts!
Since you're looking at C, I would highly suggest reading through and getting familiar with and taking inspiration from successful C language projects: sqlite, cURL, redis, memcached, haproxy and others (and perhaps the linux kernel too, but that may be skirting the line of too low-level). Granted these are closer to 'systems' level code than higher level app code, but from my experience they have a lot of translatable goodies. They all have some good idiomatic, readable, intuitive C that you can learn a bunch of good practices from. Look at how those use makefiles and build processes, possibly linters too, to their advantage and shamelessly take the good parts!
This could just be my own bias, but you might be trying to overthink it or do too much for a first project! Rather than worrying about best practices, just get your code working.
You can relish and revel in the process of improving and finessing your code later, but I'd say get to MVP stage first. Again, I might be not seeing it right here, and just adding my own bias--in this case I'm not sure, so trust your gut on what will work for you!--but at least consider the idea that diving in might be a good way to learn to swim, rather than studying Olympic freestyle technique for your first meet! :)
I'm sorry my advice is not of a more practical nature, I just considered that, in the case where you may not be oriented optimally, ensuring you point yourself in the right direction first could be crucial! Best of luck! :)
You can relish and revel in the process of improving and finessing your code later, but I'd say get to MVP stage first. Again, I might be not seeing it right here, and just adding my own bias--in this case I'm not sure, so trust your gut on what will work for you!--but at least consider the idea that diving in might be a good way to learn to swim, rather than studying Olympic freestyle technique for your first meet! :)
I'm sorry my advice is not of a more practical nature, I just considered that, in the case where you may not be oriented optimally, ensuring you point yourself in the right direction first could be crucial! Best of luck! :)
No, that's very practical advice! You're right - best to get stuck in and solve these problems as I go
Instead of -Wextra and -Wall , use a (long) list of specified warnings. This way you won't be bitten by changes in the defaults by different compiler versions.
Also, for clang, make use of the static analyzer.
https://clang-analyzer.llvm.org/
If you really mean to use the PS1 devkit, it probably uses a very old compiler. Make sure to compile your core code wit it. (It doesn't have to be the full game, or even really run on the PS1.)
If you have written a large chunk of code and only then try to compile on the old compiler, you may be in for a lot of tiny surprises. If you use plain C, this problem will be smaller than if you use C++.
C has changed just a little since the PS1 times. Modern C++ examples is another language altogether.
Also, for clang, make use of the static analyzer.
https://clang-analyzer.llvm.org/
If you really mean to use the PS1 devkit, it probably uses a very old compiler. Make sure to compile your core code wit it. (It doesn't have to be the full game, or even really run on the PS1.)
If you have written a large chunk of code and only then try to compile on the old compiler, you may be in for a lot of tiny surprises. If you use plain C, this problem will be smaller than if you use C++.
C has changed just a little since the PS1 times. Modern C++ examples is another language altogether.
- What footguns do you often see beginners trip over?
trying to use C (and I say that as someone who likes C)
it is all but impossible to write a non-trivial C program that doesn't have security vulnerabilities
libcurl is very well written C, maintained by a expert with complete mastely but still has disastrous vulnerabilities
however once accept that no-one can write secure C, if you never expose it a network, you can maybe get away with it (like for PS1)
- Does it generally matter how I do error handling, so long as it isn't setjmp / longjmp?
are you really a beginner if you know about these? :)
trying to use C (and I say that as someone who likes C)
it is all but impossible to write a non-trivial C program that doesn't have security vulnerabilities
libcurl is very well written C, maintained by a expert with complete mastely but still has disastrous vulnerabilities
however once accept that no-one can write secure C, if you never expose it a network, you can maybe get away with it (like for PS1)
- Does it generally matter how I do error handling, so long as it isn't setjmp / longjmp?
are you really a beginner if you know about these? :)
I always use p for pointers, but not p_. Mainly it's because of aesthetics when scanning code. The -> operator and the _ always break things up too much for my brain. If I'm in a well protected piece of code where I know I don't need to do null check, I may write something like this:
I do mostly agree with the other commentors though, just dig in an have fun and learn what works for you.
pFoo->pBar->pBaz=blah;
with the underscore, I find that much harder to parse (probably just my dyslexia, but maybe it's useful for you too). This is much harder for me to read. p_foo->p_bar->p_baz=blah;
Another thing that I've learned over the years is any time I need to do a malloc, make it typed-ish and have an accompanying free. Sometimes you may need to use a different library or a different allocator and then you have conflicts on who creates something and who deletes it. So I always have paired foo* new_foo();
void delete_foo(foo* pFoo);
bar* new_bar();
void delete_bar(bar* pBar);
baz* new_baz();
void delete_baz(baz* pBaz);
Those functions may simply be wrappers around a malloc/free, or they may be more complex, but I like to start with having them exist in case I need to change the implementation later. When you're managing your memory, try to make it hard to make mistakes.I do mostly agree with the other commentors though, just dig in an have fun and learn what works for you.
The second point is imho the single most important thing to know. Even if you have
foo* new_foo() { return (foo*)malloc(sizeof(foo)); }
or something like that, you should always have `new_foo` and `delete_foo` otherwise you will leak memory and you will have use after free.It's fantastic to see your enthusiasm for delving into C programming and the world of PSOne game development! As you're exploring the C language, consider adhering to best practices for memory management, like pairing every allocation with an eventual deallocation (malloc/free).
Using prefixes for globals/pointers (like g_ or p_) can help with code readability. For error handling, you might find returning error codes or using errno and perror a more conventional approach.
As for tools, consider using static code analyzers like Clang's Scan-build or AddressSanitizer to catch potential issues early.
For more insights into programming and development, you can explore Rather Labs (https://www.ratherlabs.com), where you might find valuable resources to aid your journey. Happy coding!
Using prefixes for globals/pointers (like g_ or p_) can help with code readability. For error handling, you might find returning error codes or using errno and perror a more conventional approach.
As for tools, consider using static code analyzers like Clang's Scan-build or AddressSanitizer to catch potential issues early.
For more insights into programming and development, you can explore Rather Labs (https://www.ratherlabs.com), where you might find valuable resources to aid your journey. Happy coding!
If you're just getting started, I agree with others here to just get something working. You'll naturally bump into the limitations of whatever structure/design patterns you're using.
Making Embedded Systems[0] is a great book that goes over some basics about how to structure a low level coding project. It's focused on embedded projects, but the principles will still apply.
It's been very useful for me to learn how to make the compiler, and make/cmake, work well with the software that I'm writing. Things like compile time switches can be really useful in making your software super flexible. For example, you can add in compile time switches in your code to log particular events when you're debugging that wouldn't be useful and make your project bloated in the final version.
[0]: https://www.oreilly.com/library/view/making-embedded-systems...
Making Embedded Systems[0] is a great book that goes over some basics about how to structure a low level coding project. It's focused on embedded projects, but the principles will still apply.
It's been very useful for me to learn how to make the compiler, and make/cmake, work well with the software that I'm writing. Things like compile time switches can be really useful in making your software super flexible. For example, you can add in compile time switches in your code to log particular events when you're debugging that wouldn't be useful and make your project bloated in the final version.
[0]: https://www.oreilly.com/library/view/making-embedded-systems...
Oh, about editor, I use sublime, but the difference maker is clangd. It’s the most powerful and easy to use code intelligence I’ve found for C.
Use a tool like bear or make a compile_commands.txt for it and you’ll be getting an IDE level completion on a lightweight editor!
Use a tool like bear or make a compile_commands.txt for it and you’ll be getting an IDE level completion on a lightweight editor!
I was interested in similar advice when I picked up some C projects at work. I ended up often referring to https://github.com/mcinglis/c-style
My single most important thing when starting is Don't ignore compiler warnings. Aim for zero warnings. Not always practical, but definitely investigate each one and be sure it doesn't apply in your case
Prefixes: yes
Allocations: make _alloc() and _free() functions for ADTs. Use Valgrind.
IDEs: pretty much anything from ed(1) upwards will be fine.
Modern C: just don't. There's very little in "modern" C that's actually good.
Errors: return a pointer or an int. 0 = success. If you need more info, pass an error struct pointer as the last parameter, and fill it in if it's not NULL.
Tools: compiler warnings (you should be clean with pedantic), valgrind
Allocations: make _alloc() and _free() functions for ADTs. Use Valgrind.
IDEs: pretty much anything from ed(1) upwards will be fine.
Modern C: just don't. There's very little in "modern" C that's actually good.
Errors: return a pointer or an int. 0 = success. If you need more info, pass an error struct pointer as the last parameter, and fill it in if it's not NULL.
Tools: compiler warnings (you should be clean with pedantic), valgrind
pointers and arrays. bounds checking. C is a loaded footgun, careful
don't prefix with scope letters, avoid globals. naming can be difficult, but really succinct names are so helpful.
avoid prefixing, and avoid exposing a plethora of functions or creating them in the first place. delete these five words
first in, last out. there are many strategies
sadly, whatever horrible text editor is on the machine that I am using. I am working on creating my own text editor / programming editor
Variable Length Arrays (VLA.) I had to look that up. avoid features, keep it simple. try other ways, measure the cost
it absolutely matters that error handling works. failure is not an option, it's an opportunity. handle all errors, and avoid creating more
Undefined behavior (UB.) avoid acronyms and avoid undefined behavior. use all the tools that you can muster
don't prefix with scope letters, avoid globals. naming can be difficult, but really succinct names are so helpful.
avoid prefixing, and avoid exposing a plethora of functions or creating them in the first place. delete these five words
first in, last out. there are many strategies
sadly, whatever horrible text editor is on the machine that I am using. I am working on creating my own text editor / programming editor
Variable Length Arrays (VLA.) I had to look that up. avoid features, keep it simple. try other ways, measure the cost
it absolutely matters that error handling works. failure is not an option, it's an opportunity. handle all errors, and avoid creating more
Undefined behavior (UB.) avoid acronyms and avoid undefined behavior. use all the tools that you can muster
I would try to avoid using too many macros which generate multi-line code, because preprocessed code will have different line numbers compared to the code in your editor, making it harder to follow the compilation errors and warnings.
Honestly I would prioritize building the game using your natural (today's) intuition.
I think you gain the best experience when you make mistakes and become intimately familiar with what it was like to write and edit a codebase with those mistakes, especially since you don't yet know what those mistakes are.
Only then do you really internalize advice and better solutions because you can perceive the exact problems that the better code is solving.
I think you gain the best experience when you make mistakes and become intimately familiar with what it was like to write and edit a codebase with those mistakes, especially since you don't yet know what those mistakes are.
Only then do you really internalize advice and better solutions because you can perceive the exact problems that the better code is solving.
how about using nim instead,c is the default backend, easier coding than c, I am a c programmer and now I use nim for c
Read over "Secure Coding in C and C++".
I don't have any experience with PS1 development, but I've been working on making games in C for the Playdate recently, and here's some things I've found useful:
when it comes to memory management, don't "OCD malloc()/free()"—take a second and think about the structure of your game, and plan things out accordingly. is your game going to have different levels/maps, each with different assets (map data, textures, meshes) that need to be loaded/unloaded between levels/maps?
you can use an arena allocator to allocate a big ol chunk of memory for the current level (char* total_memory = malloc(1024*1024*2)), which can then be reset without free()ing by just setting its high water mark (a size_t) to zero. you can also use an arena allocator to provide yourself a temporary, per-frame buffer—just reset its high water mark to zero at the end of each frame—when you have this, who needs garbage collection?
here's a slightly modified version of what I'm using in my Playdate games:
if you don't need per-level assets, just remove ARENA_LEVEL from the enum, and mem_use(ARENA_FRAME) after your initial game-wide loading stuff.
instead of dynamically-allocated and/or resizable arrays, consider fixed-size arrays with a size_t indicating which element can be used next, which is also the count of the elements currently in the array (starting at zero). to add an element, store it in foos[foos_count++]. to remove element n from the array, swap it with the last element of the array, and decrement the size_t.
with a little bit of forethought, you shouldn't really ever have to free() memory for a simple game. (but, again, I know nothing about how the PS1 works—maybe you actually do need to free() stuff, for some reason. I doubt it though.)
check out this article for more inspiration: https://phoboslab.org/log/2023/08/rewriting-wipeout
in general, just create data structures and write functions that operate on them. don't get fancy with abstractions—keep everything as simple as possible.
when it comes to memory management, don't "OCD malloc()/free()"—take a second and think about the structure of your game, and plan things out accordingly. is your game going to have different levels/maps, each with different assets (map data, textures, meshes) that need to be loaded/unloaded between levels/maps?
you can use an arena allocator to allocate a big ol chunk of memory for the current level (char* total_memory = malloc(1024*1024*2)), which can then be reset without free()ing by just setting its high water mark (a size_t) to zero. you can also use an arena allocator to provide yourself a temporary, per-frame buffer—just reset its high water mark to zero at the end of each frame—when you have this, who needs garbage collection?
here's a slightly modified version of what I'm using in my Playdate games:
#define TOTAL_MEMORY 1024 * 1024 * 1 // 1MB of memory (out of 4, I think, on a PS1?)
typedef enum {
ARENA_GAME,
ARENA_LEVEL,
ARENA_FRAME,
MEM_ARENA_COUNT
} Mem_Arena;
char* mem_chunk;
Mem_Arena mem_arena = ARENA_GAME;
size_t mem_end[MEM_ARENA_COUNT] = { 0 };
void mem_init() { mem_chunk = malloc(TOTAL_MEMORY); }
void* mem_alloc(size_t bytes) {
void* ptr = mem_chunk + mem_end[mem_arena];
mem_end[mem_arena] += bytes;
return ptr;
}
void mem_reset() {
for (int i = mem_arena; i < MEM_ARENA_COUNT; ++i)
mem_end[i] = (i == 0) ? 0 : mem_end[i-1];
}
void mem_use(Mem_Arena arena) {
for (int i = 0; i <= arena; ++i)
if (mem_end[i] == 0) mem_end[i] = (i == 0) ? 0 : mem_end[i-1];
mem_arena = arena;
mem_reset();
}
mem_init() at the start of the game, mem_alloc() to allocate memory in the current arena (ARENA_GAME at the start), mem_use(ARENA_LEVEL) when done loading game-wide stuff, mem_use(ARENA_FRAME) when done loading level stuff, mem_reset() at the end of the frame loop. to unload one level and load another, simply mem_use(ARENA_LEVEL), and then mem_use(ARENA_FRAME) once again when done.if you don't need per-level assets, just remove ARENA_LEVEL from the enum, and mem_use(ARENA_FRAME) after your initial game-wide loading stuff.
instead of dynamically-allocated and/or resizable arrays, consider fixed-size arrays with a size_t indicating which element can be used next, which is also the count of the elements currently in the array (starting at zero). to add an element, store it in foos[foos_count++]. to remove element n from the array, swap it with the last element of the array, and decrement the size_t.
with a little bit of forethought, you shouldn't really ever have to free() memory for a simple game. (but, again, I know nothing about how the PS1 works—maybe you actually do need to free() stuff, for some reason. I doubt it though.)
check out this article for more inspiration: https://phoboslab.org/log/2023/08/rewriting-wipeout
in general, just create data structures and write functions that operate on them. don't get fancy with abstractions—keep everything as simple as possible.
I recently acquired a very uncommon PlayStation 1 devkit and I've set about a personal sidequest to write a _very basic_ PSOne game.
I've started by prototyping in JavaScript/canvas and am now porting it to C/SDL. Once that's done I aim to port the C code to use the PSX C SDKs.
As well as hobbyism, I'm doing this to understand C better and at least grasp the practices of C programmers, even if I end up seldom writing C myself.
With that out of the way, what practices are good to internalise working in a C project?
- What footguns do you often see beginners trip over?
- Do you use prefixes like g_ or p_ for globals / pointers?
- What's your "approach" to modularisation in C? Do you prefix non-static function names to mark them as being part of a package?
- What are your preferred patterns for ensuring all allocations are eventually freed?
- What IDEs do people use for hobbyist C projects? Right now I am editing in VSCode, which is okay, but a little limited
- Will I be "okay" in the real world using more "recent" C features like VLAs? Or are these typically proscribed?
- Does it generally matter how I do error handling, so long as it isn't setjmp / longjmp?
- Are there any tools that will help me avoid many footguns or UB? I am compiling with -Wall -Wextra -Wpedantic, and using "leaks" on MacOS.
Any advice from C programmers new or seasoned is a help!