c-scripting/README.md

30 KiB

C-Scripting

Allen Webster. June 28 2026 - July 3 2026

The only way forward is inside out.

Index

  1. Start C-Scripting in your C program.
  2. What is C-Scripting?
  3. How does it work?
  4. Known pitfalls and unexplored treachery.
  5. Introduction to the Symbol Set programming primitives.
  6. Awesome things you can do with C-Scripting.
  7. The end.
  8. Change Notes.

1. Start C-Scripting in your C program.

  1. Pick a project of yours to integrate with - or start an empty "Hello World!" project to play with it.

  2. Copy the files in the symbol_set folder into your project, add that folder to your includes path.

  3. If you are on Linux compile symbol_set.ld_meta. The shell script build_ld_meta.sh builds this program with clang. You will want to be able to use the compiled program when you are building with symbol_set on Linux.

  4. In any compilation unit where you are going to be C-Scripting, #include "symbol_set.h".

  5. Before the header, #define SY__MAIN -- If you have multiple compilation units, #define SY__MAIN 1 into just one of them (perhaps main.c).

At this point your program should build and behave as it did before setup. If something went wrong for you when you tried, you can contact support@mr4th.com and I can try to help you. If it does build, then you're ready to start C-Scripting!

2. What is C-Scripting?

C-Scripting is when you're programming in C, and you've got an underlying architecture that lets you tap into features or expressivity that feel more scripting-like than C-like. I named this particular project "C-Scripting" but the spirit of the name goes back much futher. It's the idea that C doesn't have to be unproductive if you make smart decisions about how you equip yourself with tools.

By looking for new ways to engage in C-Scripting, I have had two radical transformations to my style of C. The first came from embracing Arenas and non-conflicting local scratchs. This repository contains the kernel of my second radical transformation, a data structure I call a "Symbol Set".

There are many ways to C-Script, but outside of this little discussion of the spirit of C-Scripting, this document is about C-Scripting with Symbol Sets.

What is a Symbol Set?

When you define a symbol set, you give it a name, and a type. Symbol sets live at global scope, and act like an enum and an array working together. The type given in the definition sets the type of the array elements.

When you define a symbol, you specify which symbol set it belongs to. The symbol is assigned a unique id in the enum and a corresponding slot in the array. When you define a symbol, you define the static initialization for the corresponding slot in the array. Symbols often have names, but there are also great uses for unnamed symbols. Unnamed symbols still get an id and a corresponding slot in the array, they just can't be referenced statically by other code. Usually you do this when you have no reason to use the names and it would be tedious to have to come up with pointless unique names.

You can imagine it looks sort of like this:

// define MyCommand type which I will underly the symbol set
struct MyCommand{
  String8 name;
  String8 description;
  Hook *hook;
};

// define the MY_COMMAND symbol set
SYMBOL_SET_DEFINE(MY_COMMAND, MyCommand);

// define symbols in the MY_COMMAND symbol set (ie define commands)
MY_COMMAND_DEFINE(Foo, "Do Foo."){
  do_foo(ctx);
}

MY_COMMAND_DEFINE(EasyBar, "If it's not too hard, do Bar."){
  if (difficulty(ctx) < 3){
   do_bar(ctx);
  }
}

Code structured very similarly to the above ends up with semantics that would be like having all that data, and all those function bodies organized into a single addressable table, and a corresponding enum:

void mycommand__Foo(void*){
  do_foo(ctx);
}
void mycommand__EasyBar(void*){
  if (difficulty(ctx) < 3){
   do_bar(ctx);
  }
}
enum MY_COMMAND_ID{
  MY_COMMAND_ID__NULL = 0,
  MY_COMMAND_ID__Foo,
  MY_COMMAND_ID__EasyBar,
};
MyCommand MY_COMMAND_array[] = {
  { 0 },
  { str8("Foo"), str8("Do Foo."), mycommand__Foo },
  { str8("EasyBar"), str8("If it's not too hard, do Bar."), mycommand__EasyBar }
};

You can loop over the array of symbols. And you can statically reference the symbol names to get indices into the array, or just to compare ids, create run-time lists of symbols, etc.

3. How does it work?

Implementing the Gather: Data Sections

Under the hood, symbol sets are implemented by assigning each one a special data sections.

Data sections exist in object files and executable files after you compile your code all the time, but usually we don't think about it. Typically there is a .text section that contains the executable code, a .data section that contains read-write data with non-zero initialized values, a .rdata section or .rodata for read only data, and a .bss section for read-write data that should be zero initialized, plus a handful of others that can contain information for other things like stack unwinding or debug information.

Why use data sections for this? Because that's how you get the compiler to gather up all of the global variables in the same set, and lay them out into a single array. C doesn't have a feature that works this way on purpose. But any compiler that has an extension that lets you assign a global variable to a specific data section, already contains the necessary internal logic to achieve this feature, it just couples that logic to data sections. And that's why use data sections for this.

Locating Data Sections

In order for this to work, we have to be able to find the base pointer of the array, and either the count of symbols, or the pointer to the one-past-last slot in the array.

Linux Version: Linker Scripting

On Linux the solution I have arranged for this is the program symbol_set.ld_meta. The way this program works is it scans your object files before you link them, and it generates a linker script. Then when you link your program with all those object files you also pass the linker script, and it tells the linker how to generate symbols that mark the beginning and end of each section, and as a bonus it packs all the mini data sections down into the normal .data section.

In order for the symbol_set.ld_meta program to know which data sections and symbols need to be arranged, I adopt the restriction that sections that implement symbol sets have to start with .sy.. This seems to me to be in keeping with the traditional way of using the name of the section to organize how it interacts with the "tool chain" programs.

Even deeper under the hood, the symbol_set.ld_meta program builds a list of all the sections it sees that start with .sy. and also all the unresolved symbols it sees that look like markers for symbol set ranges (syfirst__ and syopl__ prefixes). The rest is a matter of generating a linker script like this: sy.ld

SECTIONS {
 .sy : {
  syfirst__cmd = .; *(.sy.cmd); syopl__cmd = .;
 }
}
INSERT AFTER .data;

Windows Version: Extended Section Names

This section was changed from version 1.0 to 1.1. Credit Martins on the Handmade Network discord for suggesting this possibility.

On Windows the tool chain doesn't have linker scripts, but it does have one simpler feature built in that mostly gets me what I need to do the same thing. On Windows you can name a section like ".sec$extension". The part after the $ is removed by the linker before it becomes your final program. If there are multiple sections with the same base name, but different extensions, then they are concatenated together into one section and sorted alphabetically by their extensions.

So we can place a special sentinel variable at the beginning and end of each symbol set section, and use those variables to locate the symbol set.

This is roughly how the trick looks:

SEC(".cmd$myspecialsection_a") MyType myspecialsection__begin = {0};
SEC(".cmd$myspecialsection_z") MyType myspecialsection__end = {0};

SEC(".cmd$myspecialsection_m") MyType var1 = {0};
SEC(".cmd$myspecialsection_m") MyType var2 = {0};
SEC(".cmd$myspecialsection_m") MyType var3 = {0};

We have to set aside two copies of our type to setup these sentinels whose memory we never actually use, but this is the most light weight reliable solution so far.

Resolving IDs: Pointer Arithmetic

What we want is if we were to reference a symbol like MY_COMMAND.Foo that it would resolve to it's final id in the array. Unfortunately, we would need the linker to know about this and it doesn't. The linker can only patch up references to addresses because linkers don't have relocations for patching up indexes unfortunately.

But that ability to patch up addresses gets us close to what we need. Recall, from C's perspective these "symbols" are just globals or static variables. So for example MY_COMMAND_Foo might be how you name the variable that stands for the Foo symbol of the MY_COMMAND symbol set. And therefore &MY_COMMAND_Foo is the address-of that variable. But that means, with the base pointer to the symbol set, we can do pointer arithmetic to get our IDs. Think (&MY_COMMAND_Foo - MY_COMMAND__base).

Unfortunately this means that static references to these IDs are not the low level constants we would want, they resolve to a subtract between a constant and a global variable.

4. Known pitfalls and unexplored treachery.

There are a few common ways things can go wrong. The most distressing of which is that you can do everything right and still get a section that isn't the right size to reflect the number of symbols you actually created. There are a couple of ways this can happen that I have smoothed over so far.

First, sometimes small types or types with small alignments don't get packed the way you'd expect. After all, it's not really a C language array, the standard doesn't say they have to be laid out in any particular way, but I have found I can ensure proper packing by using SY__ALIGN_AS_LIT on the declaration, another wrapper for non-standard C. Then I can just round the type size up to a multiple 8 to set the underlying stride for the array. The final interface hides this from you, but it's worth noting the loss of packing efficiency for small type sizes.

Second, sometimes a bunch of null data will get appended to the end. This is "Incremental Linking". When you are building on Windows you have to pass -INCREMENTAL:NO to your linker to prevent this, or if you are using clang as a linker you pass -Xlinker -INCREMENTAL:NO.

Symbols are not necessarily packed in the order you think, or at least I wouldn't recommend depending on it if it does. Again we're dealing with non-standard behavior of C, the compiler is free to arrange global variables however it wants, and symbol sets remain perfectly useful without having to depend on order.

Symbol IDs are not serialization-ready on their own. There is no way in this scheme to ensure a symbol gets the same ID between builds. I have a number of ideas on how this can be addressed, but I haven't begun development on any of them yet, so if you start using symbol sets you're currently on your own for the serialization problem.

When you reference the ID of a symbol, it is not considered a constant expression, because it's not until after linking that the pointer arithmetic could be resolved. But since the C compiler can't see this, it rejects uses of symbol ID references that occur in statically initialized data. This is too bad, because when symbols can reference other symbols, there are so many more powerful ways to use the system. I have a work around for this limitation in the Raw handle space for referencing symbols. See SY_INIT in section 5 for more on this.

On Windows, your section names cannot be more than 8 characters long and I've already said you have to use 4 ".sy.". So you have to carefully allocate your 4 character section names when you make a new symbol set.

On Windows, your sections are not compacted together so each takes up a minimum of 4K and is allocated in 4k pages separately. I have a number of candidates for ways to eliminate this either from all builds or at least from release builds, but I haven't developed any of those ideas yet.

The usefulness of having symbols in small consecutive IDs is great, but it is constrained to singular compiled binaries (executables,.exe,.so,.dll). For instance, in a plugin system, each plugin would have it's own ID space, and therefore to reference a specific symbol you'd need to start using two-part IDs everywhere. So far I haven't developed anything to explore how this would work out.

5. Introduction to the Symbol Set programming primitives.

In the file symbol_set.h the first section contains boilerplate for setting up a symbol set.

The first 5 line block defines a symbol set. For instance if MY_COMMAND is the name of my symbol set, and MyCommand is the type, the code that instantiates the symbol set could be:

#define SYMBOL_SET_DEFINE MY_COMMAND
#define MY_COMMAND_Type    MyCommand
#define MY_COMMAND_elf_section ".sy.mcmd"
#define MY_COMMAND_coff_a_section ".sy$mcmd_a"
#define MY_COMMAND_coff_m_section ".sy$mcmd_m"
#define MY_COMMAND_coff_z_section ".sy$mcmd_z"
#define MY_COMMAND_marker  mcmd
#include "symbol_set.define.h"

The next few blocks define various patterns of helper macros for defining symbols.

The first block shows how I setup macros "data only" symbol types.

#define ZZZ_DEF(N,...) SyDefine(ZZZ, N) = { ... }

For instance you could use symbol sets to sketch out basic sprite metadata by hand. You could setup the definition helper macro like:

#define SPRITE_DEFINE(N,file,...) \
 SyDefine(SPRITES, N) = { str8(file), __VA_ARGS__ }

And then you'd use it like:

// the
SPRITE_DEFINE(Hero, "Hero.png", .x = 8, .y = 16);

The next block is how I setup macros for defining symbols with an attached function:

#define ZZZ_DEF(N,...) \
static void funczz##N(void*); \
SyDefine(ZZZ, N) = { funczz##N, ... }; \
static void funczz##N(void *ptr)

This is the version that all the "commands" example are using:

#define MY_COMMAND_DEFINE(N,desc)   \
 static void my_command__##N(Ctx*); \
 SyDefine(ZZZ, N) = { str8(#N), str8(desc), my_command__##N }; \
 static void my_command__##N(Ctx*)

The macro first declares the underlying C function, fills out the global variable initialization and inclues the C function, and then it sets up the signature of the function again, so that you can follow the macro with { ... } the function block.

MY_COMMAND_DEFINE(Foo, "Do Foo."){
 do_foo(ctx);
}

I don't have a block for this in the boilerplate, but if you want to create a symbol type that wants multiple attached functions, you can do it like this:

#define ZZZ_DEF(N,...) \
static void func1zz##N(void*); \
static void func2zz##N(void*); \
SyDefine(ZZZ, N) = { func1zz##N, func2zz##N, ... };

#define ZZZ_FUNC1(N) void func1zz##N(void*)
#define ZZZ_FUNC2(N) void func2zz##N(void*)

The final block shows how you can also setup symbols that don't have names.

#define ZZZ_DEF(...)   SyDefineUnnamed(ZZZ) = { ... }

This is useful for cases where you're not really interested in the id space of the symbol set, only the collection of values you want to put together declaratively. This can be useful for creating optional attachments to named symbols, or structures that create relationships between named symbols. For instance, you could say one symbol set defines the set of "tags" you can put on another type of symbol, think MY_COMMAND_TAG for MY_COMMAND. Then you could use unnamed symbols to set these tags:

#define MY_COMMAND_ADD_TAG(CMD,TAG) \
 SyDefineUnnamed(MY_COMMAND_TAG_LINKS) = { \
  SyRaw(MY_COMMAND, CMD), SyRaw(MY_COMMAND_TAG, TAG) }

And then you would use it like:

MY_COMMAND_ADD_TAG(Foo,FooBar);
MY_COMMAND_ADD_TAG(EasyBar,FooBar);
MY_COMMAND_ADD_TAG(EasyBar,Easy);

you can then rearrange this data into a matrix or tags lists, or whatever you need during a simple startup processing phase.

I don't really know any examples of unnamed symbols that don't rely on referencing a symbol id from another symbol, so I now need to explain how we can make this work.

Inter-Symbol References

First, we can't just store IDs into a symbol the way we'd want, because the data we store into symbols has to be statically resolved, but the IDs aren't resolvable until after linking. What we can do is use a variable address as a constant expression. Even though the final address of a global variable isn't resolved until link time, the compiler can count on the linker to fill in the gap with relocations, and so the compiler lets you treat that as a constant expression. (Take note, we need ID relocations to do better.)

Instead what we do is we store the Raw for the symbol. The Raw is just the address of the global variable typed as a SY__UAddr. SY__UAddr is an address-widthed unsigned integer. We can use SyIDFromRaw at init time to translate all of the Raws into IDs in place. It is for this reason that I put symbols in read-write memory even when I am treating them as strictly read-only after the ID fixup.

I have developed a helper in symbol_set.init.h that I use for managing this. You can include this header after symbol_set.h. It sets up a symbol set called SY_INIT and each element of this set is a function pointer that describes how to fixup the symbol set.

Using this helper, you define a symbol that references another symbol in the following way:

typedef struct MyThing{
 SY__UAddr other_thing_id;
} MyThing;

// <define a symbol set MY_THING that uses MyThing type>

#define MY_THING_DEF(N,O) \
 SyDefine(MY_THING, N) = { SyRaw(OTHER_THING, O) }

#if SY__MAIN
SY_INIT_DEF(MY_THING){
 for (SyEach(MY_THING, thing)){
  thing->other_thing_id = SyIDFromRaw(OTHER_THING, thing->other_thing_id);
 }
}
#endif

That is, I wrap the symbol reference in the helper macro to make all the symbol definitions easy on the eyes, and to ensure there that SyRaw is used and not SyID. Then if this is the compilation unit with #define SY__MAIN 1, I define a special SY_INIT symbol that describes how to loop over the symbol set and fixup SyRaws into SyIDs. I have to manually make sure the symbol set that I use in SyRaw (in the definition helper macro) matches the one I use in SyIDFromRaw (in the SY_INIT id fixup). If these don't match SyIDFromRaw just gives 0 ids.

Then I just have to call sy__run_init(); early in main. I don't use a run-before-main for this, because I already use run-before-mains to automate the process of locating symbol set arrays on Windows. These fixups can't be done until after symbols are located. SY_INIT is like run-before-main except you decide when to run all the SY_INIT functions explicitly, and therefore it can be timed strictly later than the symbol locating process. (In fact, because of all this, symbols shouldn't really be trusted during the "before main" execution phase, they should be considered ready to go at main and not necessarily before).

This way of organizing the fixups makes the maintenance of lots of interconnected symbol sets a lot more manageable than it would be otherwise. I just consider the the ID fixup boilerplate a necessary component of defining the symbol set itself, and whenever I modify the symbol set types I try to be sure to recheck the ID fixup at the same time.

Symbol Set Referencing Primitives

After defining a SYMBOL_SET here are the primitives you can use that reference it.

SyType(SYMBOL_SET) - Expands to the underlying type of the symbol set.

SyDeclare(SYMBOL_SET,N) - Forward declares the symbol N. Like any other C forward declare you may do this as many times as you want for the same name, so long as you only define it once. This is useful when you want to reference a symbol before you've defined it.

SyDefine(SYMBOL_SET,N) - Sets up the definition of the symbol N. The macro sets up the variable for the symbol, with the type from the SYMBOL_SET, and leaves the value unset, you finish the definition by writing = { ... }; after the macro.

SyDefineUnnamed(SYMBOL_SET) - Works like SyDefine except you don't specify a name.

SyAddress(SYMBOL_SET,N) - Gives the address of the symbol's slot in the set's array, typed as a pointer to the set's underlying type. Static resolution.

SyID(SYMBOL_SET,N) - Gives the 1-based ID of symbol. Literally runtime resolution, theoretically more like link-time resolution.

SyRaw(SYMBOL_SET,N) - Gives the address of the symbol typed as SY__UAddr.

SyFirst(SYMBOL_SET) SyOpl(SYMBOL_SET) - Gives the address of the first and one-past-last slot of the symbol set's array.

SyStride(SYMBOL_SET) - The stride between elements in the symbol set's array.

SyCount(SYMBOL_SET) - The number of symbols in the symbol set.

SyNext(SYMBOL_SET,addr) - Increments a pointer in the symbol set's array to the next element in the array (basically pointer arithmetic by the SyStride).

SyEach(SYMBOL_SET,var) - Expands to the control phrases of a for loop: for (SyEach(SYMBOL_SET,var)){ ... }. var is a pointer to the set's underlying type, that iterates the whole array.

SyEachID(SYMBOL_SET,id) - Expands to the control phrases of a for loop: for (SyEachID(SYMBOL_SET,id)){ ... }. id is a U32 1-based index that iterates the ids of the symbol set.

SyIDFromAddress(SYMBOL_SET,addr) - Convert a pointer to a symbol to it's ID.

SyAddressFromID(SYMBOL_SET,addr) - Convert an ID to a symbol pointer.

SyAddressCheck(SYMBOL_SET,addr) - Evaluates true if the address is inside the range of the symbol set's array.

SyIDCheck(SYMBOL_SET,id) - Evaluates true if the ID is one of the symbol IDs in the symbol set.

SyIDFromAddress_Unchecked and SyAddressFromID_Unchecked - Like SyIDFromAddress and SyAddressFromID, but skips run-time checking the parameter is in a valid range, only use if you know you've got a valid address/ID.

SyIDFromRaw and SyIDFromRaw_Unchecked same as SyIDFromAddress/_Unchecked except excepts SY__UAddr type instead of expecting pointer types.

SyFlag(SYMBOL_SET,N) - Just think (1 << SyID(SYMBOL_SET,N))

6. Awesome things you can do with C-Scripting.

Organized Command Systems

Suppose you're building some kind of editor application where you have to organize a lot of editing commands into key bindings, and menus. Further imagine it could look like this:

EDITOR_COMMAND(InsertTodo, "Insert a TODO comment."){
  U32 view_id = ed_current_view(ctx);
  U32 buffer_id = ed_buffer_from_view(ctx, view_id);
  U32 cursor_pos = ed_cursor_pos_from_view(ctx, view_id);
  ed_insert(ctx, buffer_id, cursor_pos, str8("// TODO: "));
}
EDITOR_COMMAND_FLAGS(InsertTodo, ED_CmdFlag_Write);
BIND_KEY(DefaultMap, ED_Key_T, ED_Mdfr_Alt, InsertTodo);

With a setup like this you could automatically assemble a statically defined keymap. And you could loop over all the commands you've written in your program, check their flags, string search and display their descriptions, and of course execute the command function if you have the parameters it needs.

Organized Command Line Parameters

Similarly you could use C-scripting to arrange your command line parsing. Imagine:

CMDLN_FLAG("start", "Specify the start time index for the scan. (default 0)",
           .duplicate = CMDLN_Behavior_Warning){
  out->start = cmdln_read_u32(ctx);
}
CMDLN_FLAG("end", "Specify the end time index for the scan. (default end-of-timeline)",
           .duplicate = CMDLN_Behavior_Warning){
  out->end = cmdln_read_u32(ctx);
}
CMDLN_FLAG("save", "Specify one or more intermediates to save.",
           .duplicate = CMDLN_Behavior_Concatenate){
  U32 count = rpg_read_count(ctx);
  for (U32 i = 0; i < count; i += 1){
    String8 intermediate_name = cmdln_read_idx_str8(ctx, i);
    U32 intermediate = timeline_intermediate_from_name(intermediate_name);
    if (intermediate != 0){
      timeline_add_save(out, intermediate);
    }
  }
}

State Machine

You can set up a state machine programming system in a symbol set that looks like this:

typedef struct Ctx{
  U32 state_id;
} Ctx;

///////////////////////////

STATE(GameSetup){
  game_setup(ctx);
  state_next(ctx, SyID(STATE, GameScene1));
}

STATE(GameScene1){
  scene_character(ctx, 1, Left, SyID(CHARACTER, Hero));
  scene_character(ctx, 2, Right, SyID(CHARACTER, DreamShadow));
  scene_sound_effect(ctx, SyID(SOUND_EFFECT, Eerie));
  scene_line(ctx, 1, "Huh? Is there someone there?");
  // ...
  scene_line(ctx, 2, "Will you follow me into the deep?");
  scene_offer_decision(ctx, 1, "Yes", 2, "No");
  U32 decision  = scene_player_decision(ctx);
  switch (decision){
    case 1: {
      state_next(ctx, SyID(STATE, GameScene2Deep));
    }break;
    case 2: {
      state_next(ctx, SyID(STATE, GameScene2Awake));
    }break;
  }
}

You can grab the symbol name in a macro with #N so we can save the symbol names as string data on symbols any time we'd like to, which means we can easily create an interface for running the state machine one step at a time and displaying the state, or logging each state as the machine runs, but a flat state machine is pretty limiting, so I recommend some upgrades.

Stack Machine

You can extend the state machine with a stack. When you do a state transition, you can also push a new state onto the stack. Alternatively you can pop the stack.

typedef struct Ctx{
  U32 stack[100];
  U32 stack_count;
} Ctx;

///////////////////////////

STATE(GameFight){
  if (fight_exit_condition(ctx)){
    state_pop(ctx);
  }
  else{
    fight_step(ctx);
    state_stationary(ctx);
  }
}

STATE(GameSceneTutorialBoss){
  setup_tutorial_boss(ctx);
  state_next_push(ctx, SyID(STATE, GameSceneTutorialFinished),
                  SyID(STATE, GameFight);
}

When you push a state, you also set a next state so that when you pop you don't necessarily have to go back to the state that did the push. This way state machines can be called like a subroutine.

Now when you're logging what happens you could just show the top of the stack, which gives the same effect as stepping line by line in a debugger, but at the granularity of states. You can also "backtrace" this stack and see everything that has led up to the current state, because again it's so easy to connect readable names to these states and automatically print them out.

So this above code says taht GamesTutorialBoss is a state that does some setup, and then it will appear to transition to GameFight but it does this by pushing GameFight, and at the same time, it sets GameSceneTutorialFinished as the next. This way GameFight can just pop and is generic and reusable for all sorts of contexts in the state machine, and doesn't need to hardwire itself for this particular relationship to GameSceneTutorialFinished.

Stack Frames

You can extend the state machine stack with typed frames that group states together and grant them access to shared variables. When you set the next state, the new state be a member of the same frame as the current state. But when you push a state, you may push any state, and you initialize the new frame that you push.

typedef struct StackNode{
  struct StackNode *next;
  void *data;
  U32 state_id;
  U32 pop_to;
} StackNode;

typedef struct Ctx{
  Arena *stack_arena;
  StackNode *stack;
  U32 stack_count;
} Ctx;

///////////////////////////

STACK_FRAME_STRUCT(Fight){
  CharVars entities[100];
  U32 entity_count;
  U32 player_id;
  F32 quick_time_cooldown;
  U32 quick_time_entity;
};

STATE(FightStep, Fight){
  B32 done = 0;
  U32 quick_times[100];
  U32 quick_time_entities[100];
  U32 quick_time_count = 0;
  // ... gather above variables from frame->entities ...
  
  if (done){
    state_pop(ctx);
  }
  else if (quick_time_count > 0 && frame->quick_time_cooldown == 0){
    U32 idx = rng_n(&ctx->rng, quick_time_count);
    frame->quick_time_cooldown = 5.f;
    frame->quick_time_entity = quick_time_entities[idx];
    state_next(ctx, quick_times[idx]);
  }
  else{
    frame->quick_time_cooldown = ClampBot(0.f, frame->quick_time_cooldown - ctx->dt);
    main_update(ctx, frame);
    state_stationary(ctx);
  }
}

STATE(QuickTimeZombieAttack, Fight){
  qtime_before(ctx, 3.f);
  qtime_press(ctx, Button_Left);
  qtime_press(ctx, Button_Right);
  qtime_press(ctx, Button_Left);
  if (qtime_success(ctx)){
    stun_chr(ctx, frame, frame->quick_time_entity);
    state_next(ctx, SyID(STATE, FightStep));
  }
  else if (qtime_fail(ctx)){
    hit_chr(ctx, frame, frame->player_id, 10);
    state_next(ctx, SyID(STATE, FightStep));
  }
}

Because you end up writing your own very simple main loop to run this state machine, and you implement the state_* transition functions, you can do things like track how long a state has run for, and make that information available to any of the state scripts state_timer(ctx)

If you want you could then go further and attach other interesting things to the frames in this system, like self logging:

STACK_FRAME_LOG(Fight){
  printf("entity_count = %u\n", frame->entity_count);
  printf("player = [%u] %E\n", frame->player_id, frame->entities + frame->player_id);
  printf("quick_time_cooldown = %f\n", frame->quick_time_cooldown);
}

Organized Help Information

Suppose you've got a lot of different kinds of symbols, perhaps you've got edit commands, signal filtering functions, file formats, etc. all modeled as symbol sets. Then say you decide that you'd like to start attaching more detailed help information to the symbols whose functioning is especially confusing. You can consider the following trick:

#define RuleClassXList(X) \
 X(COMMAND) \
 X(SIGNAL_FILER) \
 X(FILE_FORMAT)

typedef enum RuleClass{
  RuleClass_NULL = 0,
#define X(RC) RuleClass_##RC,
  RuleClassXList(X)
#undef X
  RuleClass_COUNT
} RuleClass;

typedef struct Rule{
  RuleClass rc;
  UAddr id;
  String8 text;
} Rule;

// <setup a rules symbol set>

#define RULE(RC,N,text) SyDefine(RC,N) = { .rc = RuleClass_##RC, .id = SyRaw(RC,N), .text = str8(text) }

#if SY__MAIN
SY_INIT(RULE){
  for (SyEach(RULE,rule)){
    switch (rule->rc){
#define X(RC) case RuleClass_##RC: rule->id = SyIDFromRaw(RC, rule->id); break;
      RuleClassXList(X)
#undef X
    }
  }
}
#endif

With this setup, you can start attaching "rules text" to any named symbol so long as you add that symbol's symbol set to the list of rule classes.

7. The end.

This is only the beginning.

There's a final interpretation of the name C-Scripting that I chose for this project. It's also a reference to "C With Classes" the predecessor to C++. In a similar way C-Scripting is an exploratory project shaping something new out of the raw materials of C. But where I want to go next with this is not a programming language.

8. Change notes.

1.1 from 1.0:

  • Changed the boilerplate for creating a symbol set.
  • Eliminated "self imaging" and switched to "sentinels" for locating symbol sets on Windows.