Fri Jul 24 15:08:00 CEST 2009
How to use C
An attempt to describe my C programming style.
The basic idea is: C isn't too bad as long as you get a hold on the
memory management. Keep data structures simple and as much as
possible use stateless code with locally managed data (C-stack).
Outside of that: try to generate C from a higher level description.
- Avoid malloc: write _functional_ C code where possible (allocate
data on the stack) and try to use a garbage collector or gc +
embedded scripting language for representing long-lived data.
This greatly simplifies interactive testing in gdb.
My experience is that in a low-level project, usually a lot of
data doesn't need malloc() for intermediate representation. The
part that does need persistence can usually be structured in such
a way that it can be queried by stateless code without the need to
transform it into alternative long-lived (cached) representations.
- If you need malloc, try to use _linear_ data structures: make
everything look like a tree or a directed graph such that
con(des)struction becomes simpler. Code that is looking at
inconsistent data because it's still building it or taking it
apart _really_ needs to be kept minimal.
- Use _comprehensions_ (iterator macros) where you would use
iterator objects or scheme-like closures. The idea is that
`downward closures' are free in C, so using them is often simpler
than writing iterator objects.
- Use _single assigment_ variables. Assign to variables at their
declaration point, and declare a new name for every intermediate
result. The compiler is smart enough to optimize this. In fact:
the internal representation in GCC is exactly this!
- Write pretty-printing routines that print data structures, but
have no other side-effects. Very useful in gdb.
- Add a TRAP = kill(getpid(), SIGTRAP) routine at all places where
you didn't implement something, instead of a print statement that
reads "not implemented". Then when running in the debugger, you
can type it in directly at the trap point and recompile.
In a sense, this advice centers around a `functional' approach to
programming, where the ability to form genuine long-lived lexical
closures is forsaken for a C-stack approximation using a few simple
If you write code that doesn't do anything except translating
descriptive data structures into more specific ones that are used by
your application: use an asynchrounous garbage collector, and manually
call it when you have the specific data. Realize that this
configuration part is really a compiler. Don't write a compiler
without GC. Alternatively: generate a simple data structure or a C
program using a HLL.
Note: these ideas about data structures of course do not apply to
problems that are inherently `algorithmic' or (mutable) data-structure
A different point: don't abuse the C preprocessor. Essentially this
means that expansions shouldn't go to deep, since that leads to
hard-to-debug code. Factor out everything that can go into an inline
function, and put the stuff you can't do in that way in a wrapper
macro. The type checker is your friend whenever you try to change
something. Macros can provide:
- implicit bindings or context, i.e. the "current" object
- abstractions over data and function definitions
- abstractions over control structures
- abstractions over (parts of) variable and function names