A Game of Life

This is going to to a quick "once over" of the code they gave as example for Conway's Game of Life. Life is a classic nerd example and is fairly straghforward.

This is more concerned with the actual effect of the code layout as opposed to the code itself.

The initial file strucure of Life looks like:

$ tree
CMakeLists.txt
main.c
Makefile
Source/
  - pdxinfo

We can basically ignore the pdxinfo for now, and the Makefile as well. CMake is more powerful than Make and allows for cleaner builds imo and its easier to decipher wtf is actually going on so we'll look at CMakeLists.txt.

Note the lines:

if (TOOLCHAIN STREQUAL "armgcc")
	add_executable(${PLAYDATE_GAME_DEVICE} ${SDK}/C_API/buildsupport/setup.c main.c)
else()
	add_library(${PLAYDATE_GAME_NAME} SHARED main.c )
endif()

include(${SDK}/C_API/buildsupport/playdate_game.cmake)

As we're focusing on binaries for the actual device we don't actually care about the else branch.

Input files

The input compilation units to this game are going to be:

  • setup.c (provided by SDK)
  • main.c (from user code)

main.c

What main does in general is entirely up to the user, but as per the docs, must contain, at a minimum:

what is setup.c?

This is where a lot of the play.date magic happens, and is a file provided in the SDK. At a high level it is putting a more accessible name of their allocator in scope, and making wrappers around malloc(), free(), and realloc(). Then is arranging the sections in the compilation unit so that the entry to your defined code is always in the same place. This simplifies their API to all games and is a really cool API decision on their part.

So how do they do all of this?

With *code*:

PDEventHandler* PD_eventHandler __attribute__((section(".capi_handler"))) = &eventHandlerShim;

extern uint32_t bssStart asm("__bss_start__");
uint32_t* _bss_start __attribute__((section(".bss_start"))) = &bssStart;

extern uint32_t bssEnd asm("__bss_end__");
uint32_t* _bss_end __attribute__((section(".bss_end"))) = &bssEnd;

.capi_handler

bss okay cool thats standard C programming stuff (initialized 0 memory), but what is .capi_handler??? According to OpenAI's ChatGPT:

The .capi_handler section is a section of memory in a program that is reserved for storing data related to C++ exception handling. This data is used by the C++ runtime library to implement the C++ exception handling mechanism.

In a C++ program, when an exception is thrown, the exception object is passed to the C++ runtime library, which searches for an exception handler that is capable of handling the exception. The runtime library uses the data stored in the .capi_handler section to determine which exception handlers are available and how to invoke them.

The .capi_handler section is created by the linker when it links a C++ program. The linker generates the .capi_handler section based on information in the object files generated by the compiler.

Cool, so it sounds like they took advantage of the fact that the input code is only C, then threw a pointer to the event handler in a section that would otherwise be unpopulated.

But what is the thing they are putting in that section? eventHandlerShim() is not a user function.

Shim code:

int eventHandlerShim(PlaydateAPI* playdate, PDSystemEvent event, uint32_t arg)
{
	if ( event == kEventInit )
		pdrealloc = playdate->system->realloc;
	
	return eventHandler(playdate, event, arg);
}

The shim makes sure that the allocator pointer is headed to the right place before any user game code executes. This is a pretty nice extra layer of developer nicities for us in that there is one less thing to worry about (the pdrealloc is used in their helper malloc(), free(), realloc(), implementations).

.bss

Also remember that in the setup.c they make a point to put a pointer to the __bss_start__ and __bss_end__ addresses in their own sections, so now we are up to 3 custom sections going into the actual build step (in addition to the default .text, .data etc).

So how is this going to look when it actually builds?