I write in C a lot (e.g.
this "Spider Game"), and it's not a very friendly language for writing data structures, to say the least.
You can do it, but there's a ton of boilerplate involved, and you need good discipline in order to keep using the same patterns, or the code becomes a mess.
So, I'm trying to canonize my coding patterns and automate the writing of them!
The result is
newfus, a language with a compiler which allows you to define data structures like this:
package: graphs
# A binary tree is either a branch, or a leaf containing arbitrary data.
union bintree:
branch: struct:
left: @bintree
right: @bintree
leaf: any
...and compile that to the following C code:
typedef struct graphs_bintree graphs_bintree_t;
typedef struct graphs_bintree_branch graphs_bintree_branch_t;
enum graphs_bintree_tag {
GRAPHS_BINTREE_TAG_BRANCH,
GRAPHS_BINTREE_TAG_LEAF,
GRAPHS_BINTREE_TAGS
};
struct graphs_bintree {
int tag; /* enum graphs_bintree_tag */
union {
graphs_bintree_branch_t* branch;
any_t leaf;
} u;
};
struct graphs_bintree_branch {
graphs_bintree_t* left;
graphs_bintree_t* right;
};
...along with automatically generated C functions for basic tasks like memory management, serialization, deserialization, etc. (Although currently only the code generation for memory management -- object "cleanup" -- is implemented.)
The major newfus types are:
- int
- bool
- byte
- string (i.e. const char *)
- array of T (compiles to a struct with capacity, length, and pointer to array of elements, plus basic array manipulation functions)
- alias (compiles to typedef)
- struct
- union (compiles to an enum of tags, plus a struct with tag and union)
- function (compiles to function pointer)
- type (compiles to const type_t *, which contains real-time type info for newfus types, including function pointers to generic methods for memory management, serialization/deserialization, etc)
- any (compiles to any_t, a struct including a const type_t *type and void *data, so that the correct generic methods for mem mgmt, serialization etc can be automatically used at runtime)
- extern CTYPE (compiles to a pointer-to-arbitrary-C-type, so you can refer to C types not defined in newfus)
The features are chosen to include everything I would need to move my existing C code over to this system.
For instance, my spider game's graphics engine is centered around a C struct called
rendergraph_t, which forms a tree of nodes, each of which may have a cached SDL surface associated with it.
So you end up with something like this in C:
typedef struct rendergraph_bitmap {
bool pbox_calculated;
position_box_t pbox;
SDL_Surface *surface;
} rendergraph_bitmap_t;
typedef struct rendergraph {
const char *name;
/* etc... */
int n_bitmaps;
struct rendergraph_bitmap *bitmaps;
/* etc... */
} rendergraph_t;
...plus a TON of boilerplate functions.
Now I can just write this:
struct rendergraph:
name: string
# etc...
bitmaps: array: struct rendergraph_bitmap:
!extra_cleanup # Calling SDL_FreeSurface(surface)
pbox_calculated: bool
pbox: inplace extern: position_box_t
surface: extern: SDL_Surface
# etc...
...and the vast majority of the boilerplate C code is generated for me, except for freeing the SDL_Surface. For that, I've got the "!extra_cleanup" part, which causes the generated code to expect a C macro "RENDERGRAPH_BITMAP_EXTRA_CLEANUP" to be defined, and which I define in a separate .h file as "
if (it->surface) SDL_FreeSurface(it->surface);" and tell the newfus compiler to
#include in its generated file.
I have no idea if anyone else would be interested in such a thing, but for me it has been fun, instructive (learned a lot about C's type system...), and possibly useful!
I'm looking forward to no longer having to write a billion lines of C code around managing the memory or printing the contents of structs like this:
typedef struct rendergraph_child {
trf_t trf;
int frame_start;
int frame_len;
int type; /* enum rendergraph_child_type */
union {
struct {
Uint8 color;
/* Weakrefs */
struct prismel *prismel;
} prismel;
struct {
int frame_i;
bool frame_i_additive;
bool frame_i_reversed;
int palmapper_n_applications;
/* Weakrefs */
struct rendergraph *rendergraph;
struct palettemapper *palmapper;
} rgraph;
struct {
const char *name;
} label;
} u;
} rendergraph_child_t;
typedef struct rendergraph {
/* etc... */
ARRAY_DECL(rendergraph_child_t, children)
/* etc... */
} rendergraph_t;
...instead just writing a single definition like this:
struct rendergraph:
# etc...
children: array: struct rendergraph_child:
trf: inplace @trf
frame_start: int
frame_len: int
type: union:
prismel: struct:
color: extern: Uint8
prismel: weakref @prismel
rgraph: struct:
frame_i: int
frame_i_additive: bool
frame_i_reversed: bool
palmapper_n_applications: int
rendergraph: weakref @rendergraph
palmapper: weakref @palettemapper
label: struct:
name: string
# etc...
For comparison, here is
rendergraph.h +
rendergraph.c, versus
geom.fus (of whose 166 lines the entire rendergraph struct takes up about 60).