roast2d_internal 0.3.0-alpha.1

Roast2D internal crate
Documentation
use std::{path::PathBuf, sync::mpsc::Receiver};

use crate::prelude::*;
use anyhow::Result;
use libloading::Library;
use notify::Watcher;

const DEFAULT_WATCH_PATH: &str = "src";
const FN_CREATE: &[u8] = b"game_create";
const FN_INIT: &[u8] = b"game_init";
const FN_UPDATE: &[u8] = b"game_update";
const FN_DRAW: &[u8] = b"game_draw";
const FN_CLEANUP: &[u8] = b"game_cleanup";

#[cfg(unix)]
use libloading::os::unix::*;
#[cfg(windows)]
use libloading::os::windows::*;

type FnCreate = unsafe extern "C" fn() -> *mut std::ffi::c_void;
type FnInit = unsafe extern "C" fn(*mut std::ffi::c_void, *mut Engine);
type FnUpdate = unsafe extern "C" fn(*mut std::ffi::c_void, *mut Engine);
type FnDraw = unsafe extern "C" fn(*mut std::ffi::c_void, *mut Engine);
type FnCleanup = unsafe extern "C" fn(*mut std::ffi::c_void, *mut Engine);

struct SymbolTable {
    lib: Library,
    create: Symbol<FnCreate>,
    init: Symbol<FnInit>,
    update: Symbol<FnUpdate>,
    draw: Symbol<FnDraw>,
    cleanup: Symbol<FnCleanup>,
}

impl SymbolTable {
    pub fn load(lib_path: &PathBuf) -> Result<Self> {
        unsafe {
            let lib = Library::new(lib_path)?;
            let create = lib.get::<FnCreate>(FN_CREATE)?.into_raw();
            let init = lib.get::<FnInit>(FN_INIT)?.into_raw();
            let update = lib.get::<FnUpdate>(FN_UPDATE)?.into_raw();
            let draw = lib.get::<FnDraw>(FN_DRAW)?.into_raw();
            let cleanup = lib.get::<FnCleanup>(FN_CLEANUP)?.into_raw();
            let st = SymbolTable {
                lib,
                create,
                init,
                update,
                draw,
                cleanup,
            };
            Ok(st)
        }
    }

    pub fn unload(self) -> Result<()> {
        self.lib.close()?;
        Ok(())
    }
}

pub struct HotReloader {
    lib_path: PathBuf,
    table: Option<SymbolTable>,
    game: *mut std::ffi::c_void,
    watcher_rx: Receiver<notify::Result<notify::Event>>,
    #[allow(dead_code)]
    watcher: notify::INotifyWatcher,
}

impl HotReloader {
    pub fn new(lib_path: PathBuf) -> Result<Self> {
        let (tx, rx) = std::sync::mpsc::channel();
        let mut watcher = notify::RecommendedWatcher::new(tx, notify::Config::default())?;

        // watch
        let path: PathBuf = DEFAULT_WATCH_PATH.into();
        watcher.watch(&path, notify::RecursiveMode::Recursive)?;
        log::info!("Start watching file changes: {path:?}");

        let table = SymbolTable::load(&lib_path)?;
        let game = unsafe { (*table.create)() };
        Ok(Self {
            lib_path,
            table: Some(table),
            game,
            watcher_rx: rx,
            watcher,
        })
    }

    pub fn compile(&self) -> Result<()> {
        let output = std::process::Command::new("cargo")
            .args(["build"])
            .output()?;
        if !output.status.success() {
            let err = String::from_utf8(output.stderr)?;
            log::error!("Failed to compile \n{err}");
        }
        Ok(())
    }

    pub fn reload(&mut self) -> Result<()> {
        if let Some(table) = self.table.take() {
            table.unload()?;
        }
        self.table = Some(SymbolTable::load(&self.lib_path)?);
        Ok(())
    }

    // Check file changes, recompile, reload
    fn try_hotreload(&mut self) {
        let mut changed = false;
        while let Ok(res) = self.watcher_rx.try_recv() {
            match res {
                Ok(event) => {
                    log::info!("Change: {event:?}");
                    if matches!(event.kind, notify::EventKind::Modify(..)) {
                        changed = true;
                    }
                }
                Err(error) => log::error!("Error: {error:?}"),
            }
        }

        if changed {
            if let Err(err) = self.compile() {
                log::error!("Compile failed {err:?}");
                return;
            }
            if let Err(err) = self.reload() {
                log::error!("Reload failed {err:?}");
            }
        }
    }
}

impl Scene for HotReloader {
    fn init(&mut self, g: &mut Engine) {
        unsafe {
            if let Some(table) = self.table.as_ref() {
                (*table.init)(self.game, g);
            }
        }
    }

    fn update(&mut self, g: &mut Engine) {
        self.try_hotreload();

        unsafe {
            if let Some(table) = self.table.as_ref() {
                (*table.update)(self.game, g);
            }
        }
    }

    fn draw(&mut self, g: &mut Engine) {
        unsafe {
            if let Some(table) = self.table.as_ref() {
                (*table.draw)(self.game, g);
            }
        }
    }

    fn cleanup(&mut self, g: &mut Engine) {
        unsafe {
            if let Some(table) = self.table.as_ref() {
                (*table.cleanup)(self.game, g);
            }
        }
    }
}