Nimbin[12]?SDL & Graphics / sdl_runtime_compiler / src/systems/dyn_loader_system.cpp

sdl_runtime_compiler git · main

SDL3 game for running and compiling code at runtime

sdl3 c++ compiler dlopen cmake · first commit 2026-04-19 · last commit 2026-07-03 (3 months ago) · synced 3 days ago · upstream: git.ide3.de/hsnr/sdl-runtime-compiler

C++ 72.3% C 26.2%
git clone https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler.gitwget https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler/archive/sdl_runtime_compiler.tar.gz
src/systems/dyn_loader_system.cpp 5.4 KB · 186 lines raw
#include <SDL3/SDL.h>
#include <dlfcn.h>
#include <string>
#include <sys/select.h>

#include "systems/dyn_loader_system.hpp"
#include "text/text.hpp"

namespace
Nimbin
{

// Where game_mod_abi.h is installed at runtime, and the build scratch dir.
static constexpr const char* MOD_INCLUDE_DIR = "/tmp/game_mod_include";
static constexpr const char* MOD_SRC_PATH    = "/tmp/game_mod.c";


// ── Harness ──────────────────────────────────────────────────────────────
// Wraps the player's spawn-helper calls in a real translation unit. The
// player's tokens become the body of game_build(); box()/text() append to
// the engine-provided output array. Bounds-checked so a player can't
// overflow `out`.
std::string
DynLoaderSystem::wrap_Source(const std::string& user_body)
{
    std::string s;
    s += "#include \"game_mod_abi.h\"\n";
    s += "#include <string.h>\n";
    s += "\n";
    s += "static DynObject* g_out;\n";
    s += "static int        g_max;\n";
    s += "static int        g_n;\n";
    s += "\n";
    s += "static void box(double x,double y,double z,"
         "double hx,double hy,double hz){\n";
    s += "    if(g_n>=g_max) return;\n";
    s += "    DynObject* o=&g_out[g_n++];\n";
    s += "    o->kind=DYN_BOX; o->x=x;o->y=y;o->z=z;\n";
    s += "    o->hx=hx;o->hy=hy;o->hz=hz;\n";
    s += "    o->r=0.55f;o->g=0.55f;o->b=0.75f; o->text[0]=0;\n";
    s += "}\n";
    s += "\n";
    s += "static void text(double x,double y,double z,const char* t){\n";
    s += "    if(g_n>=g_max) return;\n";
    s += "    DynObject* o=&g_out[g_n++];\n";
    s += "    o->kind=DYN_TEXT; o->x=x;o->y=y;o->z=z;\n";
    s += "    o->hx=o->hy=o->hz=0.0;\n";
    s += "    o->r=1.0f;o->g=1.0f;o->b=1.0f;\n";
    s += "    strncpy(o->text, t?t:\"\", sizeof(o->text)-1);\n";
    s += "    o->text[sizeof(o->text)-1]=0;\n";
    s += "}\n";
    s += "\n";
    s += "int game_build(DynObject* out,int max){\n";
    s += "    g_out=out; g_max=max; g_n=0;\n";
    s += "    /* ---- player source begins ---- */\n";
    s += user_body;
    s += "\n    /* ---- player source ends ---- */\n";
    s += "    return g_n;\n";
    s += "}\n";
    return s;
}


void
DynLoaderSystem::build(Text::Handler& out, const std::string& user_src, bool full_source)
{
    last_build_ok = false;

    out.clear();
    _count = 0;

    FILE* f = fopen(MOD_SRC_PATH, "w");
    if (!f) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                     "DynLoaderSystem: cannot write %s", MOD_SRC_PATH);
        push_Line(out, "internal error: cannot write source");
        return;
    }
    const std::string src = full_source
                          ? user_src
                          : wrap_Source(user_src);
    fputs(src.c_str(), f);
    fclose(f);

    _pending_so = "/tmp/game_mod_" + std::to_string(_build_serial++) + ".so";

    std::string cmd = "LC_ALL=C cc -shared -fPIC -O0 -I";
    cmd += MOD_INCLUDE_DIR;
    cmd += " ";
    cmd += MOD_SRC_PATH;
    cmd += " -o ";
    cmd += _pending_so;
    cmd += " 2>&1";

    if (_pipe) { pclose(_pipe); _pipe = nullptr; }

    _pipe = popen(cmd.c_str(), "r");
    if (!_pipe)
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "DynLoaderSystem: popen failed");
}


void
DynLoaderSystem::update(Text::Handler& out)
{
    if (!_pipe) return;

    char buf[512];
    int  fd = fileno(_pipe);
    fd_set set; FD_ZERO(&set); FD_SET(fd, &set);
    struct timeval tv{0, 0};   // poll, never block the frame

    while (select(fd + 1, &set, nullptr, nullptr, &tv) > 0) {
        if (!fgets(buf, sizeof(buf), _pipe)) {
            // EOF — build finished. No error lines means success.
            pclose(_pipe);
            _pipe = nullptr;
            if (_count == 0) {
                last_build_ok = true;
                _last_so_path = _pending_so;
                push_Line(out, "BUILD OK");
            }
            return;
        }
        std::string line(buf);
        if (!line.empty() && line.back() == '\n') line.pop_back();
        if (!line.empty())
            push_Line(out, line);

        FD_ZERO(&set); FD_SET(fd, &set);
    }
}


void
DynLoaderSystem::push_Line(Text::Handler& out, const std::string& line)
{
    out.append_Line(Text::ascii_Sanitize(line));
    _count++;
}


DynArray<DynObject>
DynLoaderSystem::load_Objects()
{
    DynArray<DynObject> result;
    if (_last_so_path.empty()) return result;

    if (_handle) {
        dlclose(_handle);
        _handle = nullptr;
    }

    dlerror();
    _handle = dlopen(_last_so_path.c_str(), RTLD_LAZY | RTLD_LOCAL);
    if (!_handle) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                     "DynLoaderSystem: dlopen failed: %s", dlerror());
        return result;
    }

    using BuildFn = int(*)(DynObject*, int);
    BuildFn game_build = reinterpret_cast<BuildFn>(dlsym(_handle, "game_build"));
    if (!game_build) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                     "DynLoaderSystem: dlsym(game_build) failed: %s", dlerror());
        return result;
    }

    constexpr int MAX_OBJ = 256;
    result.resize(MAX_OBJ);
    int n = game_build(result.data(), MAX_OBJ);
    if (n < 0)        n = 0;
    if (n > MAX_OBJ)  n = MAX_OBJ;
    result.resize(static_cast<size_t>(n));
    return result;
}


DynLoaderSystem::~DynLoaderSystem()
{
    if (_handle) dlclose(_handle);
    if (_pipe)   pclose(_pipe);
}

} // namespace Nimbin