Adding Raylib to Forth
I have forked pForth as pforth-raylib to add support for Raylib to the language.
Why?
Many versions of Forth include a FFI that can be used to call C libraries. Raylib’s list of official bindings has an entry for gForth, but it’s 4 years out of date and looks abandoned. It was created using SWIG, a tool to help generate bindings for the library.
Could I use SWIG or something like it to create new Raylib bindings? Probably, but I’m not going to do that. Instead, I’m adding the latest and greatest Raylib 5 directly to the language! This will hurt portability — this version of Forth will only be able to run on devices that can also run Raylib. Thankfully, Raylib runs on just about everything, including Raspberry Pis. Considering the purpose of Raylib, I find this is acceptable. This isn’t a Forth for embedded devices; this is a Forth to write desktop applications and video games. Maybe I should name it FunForth? ForthFun? DesktopForth? ForthUI? Hmm… I’ll ponder that later.
Looking at the Source Code of pForth
Take a look around the source code and see if anything stands out — or if you even understand what’s going on. (I didn’t at first!) After poking around a bit, I found this at the top of `system.fth`
What is this? Why does it exist? It makes a blank entry in the dictionary called FIRST_COLON with no body. To solve this mystery, I searched the code base for references. It’s used in see.fth, trace.fth, and clone.fth files. Looking at the code, it’s used the same way in all locations:
FIRST_COLON is being used as a marker to identify the start of the Forth defined words. All the native C code is loaded before FIRST_COLON, so their CFAs (Code Field Address) will be less than FIRST_COLON’s CFA.
I think that’s pretty cool. Maybe in the future we can upgrade see to give us more info about c functions than “is primitive defined in ‘C’ kernel.” It would be nice if it could give us a little documentation on how the function was supposed to be used or something like that.
Looking at the C code
pf_all.h looks like a good place to start. One of the first things it loads is pForth.h header, which contains the basic functions for setting up the pForth environment, such as pfDoForth, pfBuildDictionary, and pfExecIfDefined.
Next I ended up in the pf_guts.h file, there is an enum for all the word IDs, along with some interesting comments:
Why is this enum fragile? I’m not talking about the ID_EXIT and NUM_PRIMITIVES being used as start/end index. Having a special enum value for the start and end isn’t odd. But if everything uses the enum value, why does the order matter?
The comment says dictionary files will break, is that all dictionary files, or just ones created before the order change? If the dictionary files only encode the ID value, not the enum name, then loading an existing dictionary would trigger the wrong native C functions.
This brings up an interesting question. When starting pForth with no arguments, is it loading/creating a new dictionary, or is it loading one from a file? If it loads from a file, how is that file created?
Investigating the origin of Dictionary
The pfDoForth function takes two arguments: DicFileName and IfInit. Based on these, it follows one of two paths: either loading an existing dictionary or creating a new one. (Static creates the dictionary without using I/O.) These arguments are set when calling the pForth binary with flags like -i (initialize a new dictionary) and -d (load from a file).
So does that mean there is a step out there that calls ./pForth -i for us? Lets look in the Makefile.
After the application is compiled the file pforth.dic (via the variable PFORTHDIC) is created. To do this, it runs the newly compiled pForth binary with the arguments: -i system.fth This will create a new Dictionary and run the script system.fth
The resulting pforth.dic file is then moved into the same folder as the pForth binary.
And what does the system.fth script do? It completes the Forth side of the dictionary! And the first thing it defines in the dictionary? FIRST_COLON! We’ve come full circle here — pretty satisfying, right?
If you want to create the dictionary yourself without using the make file, make sure you cd into the /fth
folder. Paths are relative, so if you try to run it from outside that folder, you will encounter a bunch of file not found errors. Using the Makefile simplifies this by ensuring it runs from the /fth
folder automatically.
../platforms/unix/pforth -i system.fth
If we open pforth.dic in a hex editor, we can clearly see the text for each word being defined. This completely disproves my theory that the "ID value is encoded in the dictionary." If the text name is stored in the dictionary, then why does the enum order matter?
Continuing the Mystery
The fact that the enum order matters isn’t the most important thing, but look at everything we’ve uncovered just by digging into it! My focus on the enum was mostly by chance — it caught my eye and sparked my curiosity. And that’s the key for any developer who wants to dive deep and learn. By picking something — anything — you can set off on a journey of discovery and understanding. The exact starting point doesn’t matter, especially when you’re exploring new code. What matters is the curiosity that drives you forward, leading to insights and growth. Keep following those threads, and who knows what you’ll uncover?
How badly does it break if I change the order? To find out, I flipped the IDs for AND and ARSHIFT and created a new dictionary. The result was exactly as expected: the C code compares based on the ID value, even though the Forth code uses words. Flipping the enums in either the binary or the dictionary causes the executed functions to swap.
Adding Raylib
Now that we’ve had some time to explore the codebase and even experiment with changes to the kernel, it’s time to focus on the main task: adding Raylib.
I installed Raylib via Homebrew, so I can add it to my build with `pkg-config`:
This setup causes pForth to link with Raylib during the build process, allowing us to start using it. For a quick test, I pasted the Raylib hello world example at the top of main() and verified everything worked as expected.
Basic Window in Forth
To start, let’s convert this C example to Forth:
Here’s how it looks in Forth:
Since we don’t import any other files, we can run it from the build folder like this:
./pforth ../../examples/basic_window.fth
This gives us our first (expected) error:
init-window ? — unrecognized word!**
This means everything is working! We haven’t defined any of the new words in the dictionary yet, so the “unrecognized word” is what we want to see.
Adding new words for Raylib
How do we add a new word that invokes C code in pForth? From our earlier exploration of the code, we know that words with C implementations have IDs in the cforth_primitive_ids enum. It’s a good bet that this is where we’ll need to add new IDs for each word we’ll be creating. But before doing that, let’s see how the existing values are used, using ID_FIND as an example.
How is ID_FIND used?
- pfBuildDictionary() adds the word FIND to the dictionary.
- CreateDicEntryC() uses the enum value as the XT and converts the C string into a Forth string. It allocates 40 bytes to hold the string version of the token name, which means the word length cannot exceed 40 characters.
- pfCatch() has a switch statement with the token and body for each word implemented in C.
Now that we understand how the existing C words work, let’s break down the steps to add our own:
- Add a new ID to the cforth_primitive_ids enum.
- Call CreateDicEntryC() to add the new ID to the dictionary.
- Add a case in pfCatch() to call the C function.
To do this, I added /csrc/raylib/pf_raylib.h
and three primary #define
statements, one for each of the three places we need to modify. RAYLIB_XT_VALUES, ADD_RAYLIB_WORDS_TO_DICTIONARY, and RAYLIB_WORDS.
Using defines keeps the implementation flexible, making it easy to add new words without cluttering up the existing code. Plus, it lets us integrate Raylib into Forth while keeping everything neatly separated.
Side Note: Don’t forget to update the Makefile to include the new header files. If you skip this step, the application won’t rebuild when csrc/raylib/pf_raylib.h
changes.
Let’s try adding the first Raylib word: INIT-WINDOW.
Rebuild and try calling it!
init-window ID_INIT_WINDOW is running c code you fool!
ok
Mapping a Forth Word to a C Function
Now that we’ve added the word and it calls our custom C code, it’s time to do the mapping. We need to pull the arguments off the stack so we can pass them into the C function, and then push the return value back onto the stack.
The value at the top of the stack can be accessed with TOS, and we can pop items off the stack with M_POP or drop them with M_DROP. (I haven’t figured out yet why they start with “M_”.) We just need to call these in the correct order to get our values off the stack. The Forth standard states that the arguments in Forth should appear in the same order as the arguments in the C library.
Because the C version looks like this: InitWindow( 800, 600, “Window Title” )
the Forth version should look like this: 800 600 s” Window Title” init-window
.
So, how do we load the values from the stack? We start with TOS and call M_POP a couple of times. (For the moment, we’ll ignore the string since it requires special handling. First, we want to make sure we can get the numeric items off the stack.)
If you trigger that, you’ll get this:
800 600 3 init-window
InitWindow( 800, 600, 3 )
ok
Nice! Let’s wrap this up by handling the string. Forth has two string formats: Simple (s"
) and Counted (c"
), while C uses a null terminator (\0
). This code uses the Simple string format, so it expects a pointer (c-addr
) and length (u
). When we get strings from Forth, we need to add the null terminator at the end.
pForth has a global strach buffer gScratch
with a max length of TIB_SIZE
, so I used those the same way the file words use them.
Runing this will show the Raylib logging information, but the new window won’t open yet. We need to add a few more words before that can happen.
Finishing the Basic Window Example with LLMs
Now that we have an established working pattern, LLMs like Copilot can handle most of the word generation for us. In fact, my Copilot only required me to start typing the name of the Raylib function, and it would complete the rest. I’m happy to let Copilot handle this grunt work — it’s no different than using a tool like SWIG to generate bindings.
That said, while Copilot makes tedious tasks quick and easy, it’s not perfect. Not everything it generates works as advertised, and I had to deal with several not-so-obvious bugs. Everything produced by an LLM needs to be carefully reviewed by a human. Subtle bugs are easy to introduce — like forgetting to M_POP
in one function, which can cause subsequent functions to be slightly off, technically working, but not quite right.
What about things that don’t need C functions, like colors and flags? These can be handled on the Forth side during dictionary generation. I added an import in system.fth
to include them via the raylib.fth
file.
Done!
With all of that done, we can run the basic window example again:
./pforth ../../examples/basic_window.fth