mwolff

mwolff

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

Showing Posts 1 to 6

stefanchrobot

stefanchrobot

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.

mat-hek

mat-hek

Membrane Core Team

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).

stefanchrobot

stefanchrobot

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.

mat-hek

mat-hek

Membrane Core Team

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 erl_driver — OTP 29.0.2 (erts 17.0.2) though, so it’s private API, isn’t it?

stefanchrobot

stefanchrobot

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.

mwolff

mwolff OP

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

— All posts loaded —

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 94592 917
New
cblavier
Hey there, It’s been more than a year since we started using LiveView as our main UI library and building a whole library of UI componen...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
heathen
Quite interesting article Google brought me. Didn’t find any mentions about it here. What do you think in general? Would you use togethe...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
AstonJ
Since we have deprecated our Erlang sections (as we have dedicated Erlang Forums now) let’s add this thread for those who’d like to post ...
New
maennchen
:warning: Security advisory: Decimal DoS vulnerability A vulnerability has been published for decimal where very large exponents can cau...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews