Port communication to access SDL in C

Hello,

write for the first time in this forum and wanted a small project to practice with ports and communication between C and Elixir. I chose SDL2, because there are enough tutorials to test it. In Elixir I only saw projects via Nif so far.

As a template I used the code from the page

https://cultivatehq.com/posts/communicating-with-c-from-elixir-using-ports/

which I have adapted accordingly for my project.
So far I only put in a few commands to show the first few tutorials of

[Lazy Foo' Productions - Beginning Game Programming v2.0]
to be able to port to Elixir

This is the C - main Code

#include "sdlCom.h"
#include "myfuncs.h"
#include "myfuncs2.h"

#define MAX_READ 1024

void process_command(char *buf, int bytes_read);

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

int main(int argc, char *argv[]) {
    char buffer[MAX_READ];
    for(;;) {
        int res = poll_input();
        if(res > 0) {
          int len = to_read_length();
          if (len > MAX_READ) err(EXIT_FAILURE, "Too large message to read.");

          // len being less than zero indicates STDIN has been closed - exit
          if (len < 0) return 1;

          read_in(buffer, len);
          process_command(buffer, len);
        }
    }
}

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

void process_command(char *buf, int bytes_read) {
    int size=0, fn; 
    char **param=NULL;

    if (bytes_read > 0){    
        param=getToken(buf,':', &size); 

        if (!param) exit(1); 

        fn=atoi(param[0]);       
 
        funcPtr[fn](buf, param); 

        free(param);
    }
    else if(bytes_read < 0) {
        exit(1);
    }

}

 
Then my own Parameter - Split - Funktion

#ifndef _MYFUNCS_H
#define _MYFUNCS_H

const int  SIZE = sizeof(char*);
const int MAX_PARAM = 8;

char** getToken(const char* text, const char sep, int *anz){ // meine Parameter Split Version (ähnlich strtok)
    int x=0;
    char** token=NULL;
    char* temp = text;

    token = (char**) malloc(SIZE * MAX_PARAM); 
    if (token) { 
        token[0] = temp;
        while(*temp){ // ersetze alle Trennzeichen durch '\0'
            if (*temp == sep){
                *temp = '\0';
                token[++x] = ++temp; // Zeiger in die Token-Liste einfügen eine Stelle hinter dem NULL-Zeichen
            }
            ++temp;
        }
        *anz=x;
    }
    
    return token;
}

#endif


#ifndef _MYFUNCS2_H
#define _MYFUNCS2_H

#include <SDL2/SDL.h>

typedef inline void(*fPtr)(char*, char**);

inline void WMEM(char* mem, void* p){
    int64_t speicher = (int64_t)p;
    sprintf(mem, "%ld", speicher); 
    write_back(mem);  
}

#define GMEM(obj, var, mem)\
    SDL_##obj* var = (SDL_##obj*)atol(mem)

#define MEM(obj, var, anz)\
    SDL_##obj* var = (SDL_##obj*) malloc(sizeof(SDL_##obj) * anz)


#define FUNC(name, expr) \
    void name(char* memory, char** pm){ \
            expr \
    }

FUNC(init, int erg=SDL_Init(SDL_INIT_EVERYTHING);
           WMEM(memory, erg); 
)

FUNC(quit, SDL_Quit(); )
FUNC(createWindow, 
     SDL_Window* win = SDL_CreateWindow(pm[1], SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, atoi(pm[2]), atoi(pm[3]), SDL_WINDOW_SHOWN); 
     WMEM(memory, win); 
)
FUNC(destroyWindow, 
     GMEM(Window, win, pm[1]);
     SDL_DestroyWindow(win);  
)
FUNC(delay, SDL_Delay(atoi(pm[1])); )
FUNC(getWindowSurface, 
     GMEM(Window, win, pm[1]);
     SDL_Surface* surface=SDL_GetWindowSurface(win);
     WMEM(memory, surface); 
)

FUNC(loadBMP, 
     SDL_Surface* surface=SDL_LoadBMP(pm[1]);
     WMEM(memory, surface); 
)

FUNC(updateWindowSurface, 
     GMEM(Window, win, pm[1]);
     SDL_UpdateWindowSurface(win); 
)

FUNC(blitSurface,
     GMEM(Surface, image, pm[1]);
     GMEM(Rect, srcRect, pm[2]);
     GMEM(Surface, screen, pm[3]);
     GMEM(Rect, dstRect, pm[4]);
     SDL_BlitSurface(image, srcRect, screen, dstRect);
)

FUNC(freeSurface,
     GMEM(Surface, surface, pm[1]);
     SDL_FreeSurface(surface); 
)

FUNC(createRect,
     MEM(Rect, rect, 1);//SDL_Rect* rect = (SDL_Rect*) malloc(sizeof(SDL_Rect));
     rect->x=atoi(pm[1]);
     rect->y=atoi(pm[2]);
     rect->w=atoi(pm[3]);
     rect->h=atoi(pm[4]);
     WMEM(memory, rect);
)

FUNC(freeRect,
     GMEM(Rect, rect, pm[1]);
     free(rect); 
)

FUNC(pollEvent,
     SDL_Event e;
     int i = SDL_PollEvent(&e);
     WMEM(memory, i);
     WMEM(memory, e.type);
     WMEM(memory, e.key.keysym.sym);
)

FUNC(getKeyName,
     const char* name = SDL_GetKeyName(atoi(pm[1]));
     strcpy(memory, name); memory[strlen(name)+1]='\0'; 
     write_back(memory);
)

fPtr funcPtr[]={init,quit,createWindow,destroyWindow,delay,getWindowSurface,
                loadBMP, updateWindowSurface, blitSurface, freeSurface,
                createRect, freeRect, pollEvent, getKeyName}; 

#endif

Elixir - Code for SDL 

defmodule ESDL.Event do
    def type() do
        %{QUIT: 0x100, KEYDOWN: 0x300, KEYUP: 0x301,
          MOUSEMOTION: 0x400, MOUSEBUTTONDOWN: 0x401,
          MOUSEBUTTONUP: 0x402, MOUSEWHEEL: 0x403}
    end
end

defmodule ESDL do
    def initModule() do Port.open({:spawn, "./sdlCom"}, [{:packet, 2}]) end

    def get(p) do
        receive do
         {^p, {:data, result}} -> result
        end
    end

    def init(p) do
        Port.command(p, ["0"])
        List.to_integer(get(p))
    end
    def quit(p) do Port.command(p, ["1"]) end
    def createWindow(p, title, width, height) do
        Port.command(p, ["2:#{title}:#{width}:#{height}"])
        get(p)
    end

    def destroyWindow(p, win) do Port.command(p, ["3:#{win}"]) end
    def delay(p, t) do Port.command(p, ["4:#{t}"]) end

    def getWindowSurface(p, win) do
        Port.command(p, ["5:#{win}"])
        get(p)
    end
    def loadBMP(p, pic) do
        Port.command(p, ["6:#{pic}"])
        get(p)
    end
    def updateWindowSurface(p, win) do Port.command(p, ["7:#{win}"]) end
    def blitSurface(p, src, srcRec, dst, dstRec) do Port.command(p, ["8:#{src}:#{srcRec}:#{dst}:#{dstRec}"]) end
    def freeSurface(p, surface) do Port.command(p, ["9:#{surface}"]) end
    def createRect(p, x, y, width, height) do
        Port.command(p, ["10:#{x}:#{y}:#{width}:#{height}"])
        get(p)
    end
    def deletRect(p, ptrRect) do Port.command(p, ["11:#{ptrRect}"]) end
    def pollEvent(p) do
        Port.command(p, ["12"])
        i = List.to_integer(get(p))
        type = List.to_integer(get(p))
        key = List.to_integer(get(p))
        {i, type, key}
    end
    def getKeyName(p, keyCode) do
        Port.command(p, ["13:#{keyCode}"])
        get(p)
    end
end

defmodule ESDL.Loop do
    def start(p, win, image, screen, loop) do
        if (loop > 0) do
            ESDL.delay(p,10)
            ESDL.blitSurface(p, image, NULL, screen, NULL)
            ESDL.updateWindowSurface(p, win)
            {_i, event, key} = ESDL.pollEvent(p)
            #keyName = ESDL.getKeyName(p, key);
            if (event != ESDL.Event.type()[:QUIT] && key != 32) do
                if (event == ESDL.Event.type()[:KEYDOWN]) do
                    IO.puts "#{key} pressed!"
                end
                start(p, win, image, screen, loop)
            end
        end
    end
end

 p= ESDL.initModule()
result = ESDL.init(p)
if result == 0 do
    win = ESDL.createWindow(p,"SDL-Test", 640, 480)
    screen = ESDL.getWindowSurface(p, win)
    image = ESDL.loadBMP(p,"./hello_world.bmp")
    ESDL.Loop.start(p, win, image, screen, 1)
    ESDL.freeSurface(p, image)
    ESDL.destroyWindow(p,win)
    ESDL.quit(p)
end

best regards
Michael

Hi, Michael! I’m currently playing around with SDL2 as well! I’ve tried both the NIF and Ports path. Both present their own challenges. Ports are going to be slower in terms of performance and this is going to be noticeable when you start making more SDL API calls. On the other hand, NIFs can crash the whole BEAM process. Additionally, some SDL API calls should happen from the main application thread, which is not something that you can easy control when calling in from an Elixir.

Have a look at unifex and bundlex.

Hi @mwolff, here’s the Membrane SDL video player implemented with C node. This use case wouldn’t work without main thread (at least on darwin afair).

Thanks for the link @mat-hek, I drew some inspiration from your package.

The use case you mentioned could use NIFs, since you can steal the main thread, the same way that the wxWidgets bindings do (that’s what I do as well). The problem is that you still have to marshall most (all?) calls into the main thread, so there’s a lot of ceremony.

1 Like

Nice, I read somewhere about wxWidgets stealing the main thread, but wasn’t aware someone else actually uses that :wink: This seems not to be documented in https://erlang.org/doc/man/erl_driver.html though, so it’s private API, isn’t it?

It is. On non-Macs wx_driver just creates a fresh thread instead of stealing it. Here’s the relevant excerpt from my code:

static int run_thread = 1;
static ErlDrvTid sdl_thread_id;

static void* sdl_main_loop(void* arg) {
  while (run_thread) {
    // Do your stuff.
  }

  return NULL;
}

// OTP is hiding this, but it's there.
// The functions are implemented in
//
//    otp/erts/emulator/beam/erl_drv_thread.c.
//
// These are only used by wxErlang, so it declares them ad-hoc in
//
//    otp/lib/wx/c_src/wxe_main.cpp
//
// and so we do the same thing.

// #ifdef __DARWIN__
int erl_drv_stolen_main_thread_join(
    ErlDrvTid tid,
    void** respp);

int erl_drv_steal_main_thread(
    char* name,
    ErlDrvTid* dtid,
    void* (*func)(void*),
    void* arg,
    ErlDrvThreadOpts* opts);
// #endif

static int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) {
  erl_drv_steal_main_thread(
    name,
    &sdl_thread_id,
    sdl_main_loop,
    NULL,
    NULL
  );
  
  // ...

  return 0;
}

The awkward part is that you now need to marshal the NIF calls into that thread. I’m using mutexes to do that. The NIF implementation is not thread-safe so you need to synchronize all NIF calls via a process. SDL is not thread-safe so you’d have to do that either way.

1 Like

Hello @stefanchrobot ,

i had started with nif, but it is not yet working as it should. Every time you have to look how you change this and that with the nif library, I find it a bit cumbersome. The examples that you find are mostly designed in such a way that they always work, but not for more complicated things.
With port everything is a string, change it to a pointer and it works if you did everything right. I changed the example code with ports to function pointers, so that the function is called with the given number. No complicated if-else stories, which also runs faster.

best regards

Michael