[index]

Anton's Research Ramblings

Managing Code Projects Part One: Header Files

I'll write a few blog posts to share some experience about working on larger projects or coordinating with teams. I want to start out easy so here's a little aside about avoiding code tangles. Even projects that start off simple can quickly become quite complicated to follow as they grow, and as more people work on parts of them.

How Did This All Get So Tangled?

Structuring code in projects turns out to be one of the hardest things to learn how to do well. I mean it takes many years re-inventing the wheel and being exposed to the ideas of others before you start to get it right. Project structure is largely a problem about clearly communicating with other people. You'll often [read: almost always] work on older, and sometimes very old, codebases that are extremely difficult to follow. I'm talking about LEGACY CODE. I saw a great definition of this zip past on Twitter recently "Legacy code is code you are afraid to change." It's very easy to blame people who worked on code that is a mess, but that's not fair - there are good reasons why messy code exists, and it may be inevitable:


This popular Internet image could probably be titled "A typical project: Day 1, Day 365."

A real conundrum with contemporary software development is that software projects are never 'finished'. We do 'software as a service' or Agile continuous delivery. The requirements change frequently, which mean design must be flexible to change. But many of our tools and techniques were designed assuming the entire project had been fully specced out in advance. As an industry-standard OOP project evolves it quickly collapses into a birds nest of everything-includes-everything else. Eventually you reach a log-jam state where there is no nice way to structure things, and you either have to scrap it, spend a good deal of time and money re-thinking structure, or do what most programmers do, and just write in shortcuts/pointers/friend relationships to cross around their own object structure. Most people in tech don't actually work on a project, or at a company, for more than a year or two, so can conveniently avoid ever dealing with this and then move to the next start-up!

Reducing Relationships Between Files

A good principle to keep your software flexible to change - which is the key to longevity - is to reduce relationships between components - try to keep modules/classes/files/functions more or less stand-alone. I can talk about inheritance and composition later, but let's look at something simpler first: header files in C and C++.

Header files are a language quirk, and understanding what to do with them for a practical purpose is a bit mixed. Here are common conventions:

Some sort of convention is useful to reduce the cognitive overhead of anyone working on the project. Pick any of the top three - they all have their merits. Add the following concepts - they will help immensely to keep things simple:

You'll find that this will significantly reduce the complexity of growing projects. Try to stick to a single types.h file as long as you can, rather than one per module, or it will be more difficult to manage. If you could draw a diagram of relationships between files it would be quite a lot simpler now, yes? If you're just using a single header file for everything then this isn't a concern, but I don't think that's everyone's favourite on team projects. I think you can drop most developer guidelines or rules, but this keep-the-headers-stand-alone one should be helpful to keep and gently enforce.

You can take this principle further and work towards most .c files being able to compile on their own with no or minimal header includes. Try adding a main() to the bottom of the file and calling some of your functions. Then you'll also find it easy to reason about the logic, especially from a unit testing point of view, since it's all in one place: data in, logic, data out.

Here's a `types` header snippet from one of my projects, and a header that uses the types.

types.h
// Copyright Anton Gerdelan <antonofnote@gmail.com>. 2019
#pragma once
#include <stdbool.h>
#include <stdint.h>

// ---------------------------------------------------------------------------------
//                                           MATH
// ---------------------------------------------------------------------------------
typedef struct vec2 {
  float x, y;
} vec2;
typedef struct vec3 {
  float x, y, z;
} vec3;
typedef struct vec4 {
  float x, y, z, w;
} vec4;
typedef struct ivec3 {
  int x, y, z;
} ivec3;
typedef struct mat4 {
  float m[16];
} mat4;
typedef struct versor {
  float w, x, y, z;
} versor;

// ---------------------------------------------------------------------------------
//                                           VOXELS
// ---------------------------------------------------------------------------------
typedef struct voxel_coords_t {
  uint32_t world_tile_x, world_tile_y;
  uint32_t tile_idx;
  uint32_t chunk_x, chunk_y;
  uint32_t layer;
  uint32_t voxel_in_chunk_x, voxel_in_chunk_y;
} voxel_coords_t;

...
props.h
// Copyright Anton Gerdelan <antonofnote@gmail.com>. 2019
#pragma once
#include "types.h"

typedef enum prop_types_t { PROP_TYPE_STAIRS = 0, PROP_TYPE_MAX } prop_types_t;

void init_props();

void free_props();

/** creates a prop on top of the given tile */
void create_prop_on( prop_types_t type, voxel_coords_t coords );

void draw_prop_guide_at( voxel_coords_t coords, const cam_t* cam );

...