Nimbin[12]?SDL & Graphics / sdl_runtime_compiler / src/systems/program_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/program_system.cpp 5.6 KB · 198 lines raw
#include <SDL3/SDL.h>
#include <cerrno>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/wait.h>

#include "entities/world.hpp"
#include "systems/program_system.hpp"
#include "text/text.hpp"
#include "utils/config.hpp"

namespace
Nimbin
{

static const char* kProgramPath = "/tmp/game_prog";


ProgramSystem::~ProgramSystem() { close_Child(); }


void
ProgramSystem::close_Child()
{
    if (_in_fd  >= 0) { ::close(_in_fd);  _in_fd  = -1; }
    if (_out_fd >= 0) { ::close(_out_fd); _out_fd = -1; }
    if (_child > 0) {
        int status;
        if (waitpid(_child, &status, WNOHANG) == 0) {
            kill(_child, SIGKILL);
            waitpid(_child, &status, 0);
        }
        _child = -1;
    }
    _pending.clear();
}


void
ProgramSystem::run(Text::Handler& out)
{
    out.clear();
    _count = 0;

    close_Child();

    if (access(kProgramPath, X_OK) != 0) {
        push_Line(out, "no program - compile first");
        return;
    }

    signal(SIGPIPE, SIG_IGN);

    int in_pipe[2]  { -1, -1 };   // parent writes [1] → child reads [0] (stdin)
    int out_pipe[2] { -1, -1 };   // child writes [1] → parent reads [0] (stdout)
    if (pipe(in_pipe) != 0 || pipe(out_pipe) != 0) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "ProgramSystem: pipe() failed");
        return;
    }

    pid_t pid = fork();
    if (pid < 0) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "ProgramSystem: fork() failed");
        ::close(in_pipe[0]);  ::close(in_pipe[1]);
        ::close(out_pipe[0]); ::close(out_pipe[1]);
        return;
    }

    if (pid == 0) {
        // ── Child ────────────────────────────────────────────────────────
        dup2(in_pipe[0],  STDIN_FILENO);
        dup2(out_pipe[1], STDOUT_FILENO);
        dup2(out_pipe[1], STDERR_FILENO);
        ::close(in_pipe[0]);  ::close(in_pipe[1]);
        ::close(out_pipe[0]); ::close(out_pipe[1]);
        // stdbuf -oL -eL line-buffers the child so a prompt printed before it
        // blocks on input still reaches the wall. LC_ALL=C for stable output.
        execl("/bin/sh", "sh", "-c",
              "LC_ALL=C stdbuf -oL -eL /tmp/game_prog", (char*)nullptr);
        _exit(127);   // exec failed
    }

    // ── Parent ─────────────────────────────────────────────────────────────
    ::close(in_pipe[0]);    // we only write the child's stdin
    ::close(out_pipe[1]);   // we only read the child's stdout
    _in_fd  = in_pipe[1];
    _out_fd = out_pipe[0];
    _child  = pid;

    // Non-blocking both ways so neither read nor write can stall the frame.
    for (int fd : { _out_fd, _in_fd }) {
        int fl = fcntl(fd, F_GETFL, 0);
        if (fl != -1) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
    }
}


void
ProgramSystem::update(Text::Handler& out)
{
    if (_out_fd < 0) return;

    char buf[512];
    fd_set set;
    struct timeval tv{0, 0};   // poll, never block the frame

    for (;;) {
        FD_ZERO(&set); FD_SET(_out_fd, &set);
        int s = select(_out_fd + 1, &set, nullptr, nullptr, &tv);
        if (s <= 0) break;   // nothing ready this frame

        ssize_t n = read(_out_fd, buf, sizeof(buf) - 1);
        if (n > 0) {
            buf[n] = '\0';
            _pending += buf;

            // Emit each complete line; keep any trailing partial for next read.
            size_t nl;
            while ((nl = _pending.find('\n')) != std::string::npos) {
                std::string line = _pending.substr(0, nl);
                _pending.erase(0, nl + 1);
                if (!line.empty() && line.back() == '\r') line.pop_back();
                push_Line(out, line);
            }
        }
        else if (n == 0) {
            // EOF — program finished. Flush any partial line and reap.
            if (!_pending.empty()) {
                push_Line(out, _pending);
                _pending.clear();
            }
            if (_count == 0) push_Line(out, "(no output)");
            close_Child();
            return;
        }
        else {
            break;   // EAGAIN / no data right now (or a read error)
        }
    }
}


void
ProgramSystem::feed_Input(const std::string& text)
{
    if (_in_fd < 0) return;   // nothing is running

    std::string line = text;
    line += '\n';            // terminate the line so scanf/cin/getline unblock

    size_t off = 0;
    while (off < line.size()) {
        ssize_t w = write(_in_fd, line.data() + off, line.size() - off);
        if (w > 0) { off += static_cast<size_t>(w); continue; }
        break;   // EAGAIN (pipe full) or EPIPE (child closed stdin) → give up
    }
}


void
ProgramSystem::echo_Input(Text::Handler& out, const std::string& line)
{
    push_Line(out, line);   // scrolls up, lands at the prompt row
}


void
ProgramSystem::echo_Words(Text::Handler& out, DynArray<Text::TokenPtr>& words)
{
    if (words.empty()) return;

    // The flown word meshes are discarded; we echo their joined text as one
    // output line (append handles the scroll + colour/placement).
    std::string line;
    for (const Text::TokenPtr& w : words) {
        if (!w || w->type != ObjectType::Text) continue;
        if (!line.empty()) line += ' ';
        line += w->text;
    }
    words.clear();

    if (!line.empty()) {
        out.append_Line(line);
        _count++;
    }
}


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

} // namespace Nimbin