Retropikzel's blog - 2025-02-14 - r7rs-pffi SDL2 example

Hello, do you want to make games with Scheme? Well then you are in luck as I got just the thing for you. We will be making a small start of a game using r7rs-pffi and SDL2 and I’m going to show you how to set everything up on Linux and Windows, how to install and use r7rs-pffi and all that. What I wont show you is how to make a (full/finished) game however, that is then up to you. :)

It helps if you are familiar with C and SDL2, but should not be strictly necessary. Familiarity with Scheme is assumed.

The full code can be found in https://git.sr.ht/~retropikzel/r7rs-pffi-example-sdl2.

If you have any questions please dont hesitate to ask on the mailing lists.

Table of contents

Choosing a Scheme implementation

Which Scheme implementations are supported is listed in the support table on projects page, any implementation in finished or beta stage should be okay to use for this but I’m only going to showcase the usage of five implementations which are Guile, Racket, Sagittarius, Chicken 5 and Kawa.

NOTE: As of writing Chicken 5 and Kawa are in Alpha status, Kawa works for this tutorial. Chicken 5 can not pass structs by value, so you can not set the colors of fonts, for example, correctly. If you are in the future reading this then try Chicken 6.

If you dont know what implementation to choose I recommend Guile if you are on Linux and Sagittarius if you are on windows. Other implementations if they are more familiar to you.

Scheme implementation setup

Debian/Ubuntu/Mint

Guile

As of writing the guile 3 version in Debian repos is too old so we are going to build Guile from source. If you are reading this in the future you can first try:

apt install guile-3.0

and see if the version in your linux distribution works.

If your version is too old, here are the build instructions, otherwise skip to next part.

To build:

apt install build-essential make libgmp-dev libunistring-dev pkg-config autoconf flex automake libtool gperf texinfo
git clone https://git.savannah.gnu.org/git/guile.git --depth=1
cd guile
./autogen.sh
./configure
make
make install

Racket

apt install racket
raco pgk install --auto r7rs

Sagittarius

Download the newest release from https://bitbucket.org/ktakashi/sagittarius-scheme/downloads/ then run:

apt install cmake make build-esential libgc-dev zlib1g-dev libffi-dev libssl-dev
tar -xf TARFILE
cd EXTRACTED_DIR
mkdir build
cd build
cmake ..
make
make install
Chicken 5

Download the latest release from https://code.call-cc.org/

tar -xf TARFILE
cd EXTRACTED_DIR
make
make install
chicken-install r7rs
Kawa

As Kawa needs Java version 22 or higher we are going to use the .jar file, download kawa files from https://www.gnu.org/software/kawa/Getting-Kawa.html. Take the precompiled version, extract it and copy file kawa-N.N.N/lib/kawa.jar to your projects root directory.

Then install recent enough Java, we are going to use sdkman for that as Debian repositories as time of writing only has Java 17.

apt install curl zip unzip
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 23-open

Windows

Download either Sagittarius or Racket and install.

SDL2 Setup

Debian/Ubuntu/Mint

apt install libsdl2-2.0-0 libsdl2-image-2.0-0 libsdl2-ttf-dev libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev

Windows

Download the .ddl files from latest release from https://github.com/libsdl-org/SDL/releases.

It’s the one with mingw in it’s name. Copy the file from SDL2-N.N.NN/x86_64-w64-mingw32/bin/SDL2.dll to your projects root directory.

Do the same for SDL2 image https://github.com/libsdl-org/SDL_image/releases and SDL2 ttf https://github.com/libsdl-org/SDL_ttf/releases.

Be sure to download the latest version in 2 series, not 3.

r7rs-pffi library setup

Download the latest version from https://git.sr.ht/~retropikzel/r7rs-pffi/refs and copy the directory called “retropikzel” into your projects root directory.

Running the code

Links:

First we need to make sure you can run the code and that everything gets loaded properly. Insert into file called main.scm this:

(import (scheme base)
        (scheme write)
        (retropikzel r7rs-pffi))


(pffi-init)
(define sdl2
  (pffi-shared-object-auto-load (list "SDL2/SDL.h")
                                "SDL2-2.0"
                                '((additional-versions . "0"))))
(define sdl2-image
  (pffi-shared-object-auto-load (list "SDL2/SDL_image.h")
                                "SDL2_image-2.0"
                                '((additional-versions . "0"))))

(define sdl2-ttf
  (pffi-shared-object-auto-load (list "SDL2/SDL_ttf.h")
                                "SDL2_ttf-2.0"
                                '((additional-versions . "0"))))

(display "Hello world")
(newline)

Then we run the code to test that our setup is correct. It should only print “Hello world”.

Guile

guile --r7rs -L . main.scm

Racket

Linux:

racket -I r7rs -S . --script main.scm

Windows:

racket.exe -I r7rs -S . --script main.scm

Sagittarius

Linux:

sash -r -L . main.scm

Windows:

sash.exe -r -L . main.scm

Chicken 5

First compile the r7rs-pffi library:

cp retropikzel/r7rs-pffi.sld retropikzel.r7rs-pffi.sld
csc -X r7rs -R r7rs -I retropikzel -s retropikzel.r7rs-pffi.sld

And then the main.scm:

csc -X r7rs -R r7rs -L -lSDL2 -L -lSDL2_image -L -lSDL2_ttf main.scm

Then run the main executable:

./main

You only need to compile the library once.

Kawa

Linux:

java -jar kawa.jar -Dkawa.import.path=*.sld main.scm

Windows:

java.exe -jar kawa.jar -Dkawa.import.path=*.sld main.scm

Running on any of these five implementations at this point should output “Hello world”.

Opening window with SDL2

Links:

First thing to do in SDL2 is to call SDL_Init, open a window and to create a renderer, in C code it would look like this:

SDL_Init( SDL_INIT_VIDEO | SDL_INIT_EVENTS );
SDL_Window *window = SDL_CreateWindow( "Hello World",
    SDL_WINDOWPOS_UNDEFINED,
    SDL_WINDOWPOS_UNDEFINED,
    x, y, SDL_WINDOW_SHOWN );
SDL_Renderer *renderer =
  SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
SDL_RenderClear(renderer);
SDL_RenderPresent(renderer);

We need to call these same functions, but before that we need to tell pffi how to do that and what kind of arguments they take:

(pffi-define sdl-init sdl2 'SDL_Init 'int (list 'int))
(pffi-define sdl-ttf-init sdl2-ttf 'TTF_Init 'int (list))
(pffi-define sdl-create-window sdl2 'SDL_CreateWindow 'pointer (list 'pointer 'int 'int 'int 'int 'int))
(pffi-define sdl-create-renderer sdl2 'SDL_CreateRenderer 'pointer (list 'pointer 'int 'int))
(pffi-define sdl-render-clear sdl2 'SDL_RenderClear 'int (list 'pointer))
(pffi-define sdl-render-present sdl2 'SDL_RenderPresent 'int (list 'pointer))

Calling pffi-define with desired name, shared object we loaded before, C name, return type and argument types creates a new function with the given name. In the first lines case the function is called sdl-init and we can call it like any normal Scheme function.

Lets call them:

(sdl-init 32)
(sdl-ttf-init)
(define window (sdl-create-window (pffi-string->pointer "Hello world") 0 0 width height 4))
(define renderer (sdl-create-renderer window -1 2))
(sdl-render-clear)
(sdl-render-present)

Adding these to the code you should now have:

(import (scheme base)
        (scheme write)
        (retropikzel r7rs-pffi))


(pffi-init)
(define sdl2
  (pffi-shared-object-auto-load (list "SDL2/SDL.h")
                                "SDL2-2.0"
                                '((additional-versions ("0")))))
(define sdl2-image
  (pffi-shared-object-auto-load (list "SDL2/SDL_image.h")
                                "SDL2_image-2.0"
                                '((additional-versions ("0")))))

(define sdl2-ttf
  (pffi-shared-object-auto-load (list "SDL2/SDL_ttf.h")
                                "SDL2_ttf-2.0"
                                '((additional-versions ("0")))))

(pffi-define sdl-init sdl2 'SDL_Init 'int (list 'int))
(pffi-define sdl-ttf-init sdl2-ttf 'TTF_Init 'int (list))
(pffi-define sdl-create-window sdl2 'SDL_CreateWindow 'pointer (list 'pointer 'int 'int 'int 'int 'int))
(pffi-define sdl-create-renderer sdl2 'SDL_CreateRenderer 'pointer (list 'pointer 'int 'int))
(pffi-define sdl-render-clear sdl2 'SDL_RenderClear 'int (list 'pointer))
(pffi-define sdl-render-present sdl2 'SDL_RenderPresent 'int (list 'pointer))

(sdl-init 32)
(sdl-ttf-init)
(define window (sdl-create-window (pffi-string->pointer "Hello world") 0 0 400 400 4))
(define renderer (sdl-create-renderer window -1 2))
(sdl-render-clear renderer)
(sdl-render-present renderer)

(display "Hello world")
(newline)

Now when you run the code you should see a window appear and disappear very fast, and words “Hello world” printed out on the terminal.

In the code we create a new window, a renderer for it and clear the renderer then show it. Some values in the code need more explanation.

Whats that 32 in (sdl-init 32)? Where does it come from? The answer is that it’s the value of SDL_INIT_VIDEO. Since the pffi can not get values from C code that are defined with #define we need to dig them up using C code. Here is the code I used:

#include <stdio.h>
#include <SDL2/SDL.h>

int main() {
    printf("Value is: %u\n", SDL_INIT_VIDEO);
}

If you put it into file called value_extractor.c then you can compile and run it with:

gcc -o value_extractor value_extractor.c
./value_extractor

Another option is to look the value up on the header file and then convert it from hex to decimal.

How about (sdl-create-renderer window -1 2)? From SDL_CreateRenderer. The first argument is “the index of the rendering driver to initialize” and -1 tells SDL to pick first one. The second one are flags again. The value we pass is SDL_RENDERER_PRESENTVSYNC and you can print it in C code with printf(“Value is: %u”, SDL_RENDERER_PRESENTVSYNC);.

How about (sdl-create-window (pffi-string->pointer “Hello world”) 0 0 400 400 4))? From SDL_CreateWindow wiki you can see that those are the title, x position, y position, width, height and flags. The flag value is SDL_WINDOW_SHOWN. You can print it in C code with printf(“Value is: %u”, SDL_WINDOW_SHOWN);.

The C function expects the title as type char*, so we convert our Scheme string “Hello world” to C pointer.

Main game loop

Now that we got to window to shortly appear it’s time to make it not disappear so fast, after all most games usually last more than 10ms. For that we need to create a function which we will call “main-loop” that calls itself, but we alsow need to handle SDL2 events inside. Otherwise our code will run in endless loop forever. So lets define a function to do that.

(pffi-define sdl-poll-event sdl2 'SDL_PollEvent 'int (list 'pointer))

SDL_PollEvent takes as agument a pointer to enum SDL_Event. Since it is and enum we need to