HACKER Q&A
📣 jbreckmckye

Good practices for my first C project?


Greeting Hackers,

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!


  👤 gavinhoward Accepted Answer ✓
* +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.


👤 mbivert
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/


👤 _benj
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!


👤 user8501
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.


👤 osivertsson
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 ;)


👤 boricj
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.


👤 cracrecry
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).


👤 pengaru
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.


👤 theideaofcoffee
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!


👤 keepamovin
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! :)


👤 actionfromafar
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.


👤 blibble
- 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? :)


👤 michaelrpeskin
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:

    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.


👤 ezedv
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!


👤 analogwzrd
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...


👤 _benj
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!


👤 AlbinoDrought
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

👤 __d
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


👤 mmphosis
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


👤 nurettin
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.

👤 hombre_fatal
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.


👤 synergy20
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

👤 nobodyandproud
Read over "Secure Coding in C and C++".

👤 niccl
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

👤 adamrezich
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:

    #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.