roast2d_internal 0.3.6

Roast2D internal crate
Documentation
use std::{
    fs,
    path::{Path, PathBuf},
    sync::mpsc::{Receiver, Sender},
    time::{Duration, Instant},
};

use crate::prelude::*;
use anyhow::Result;
use libloading::Library;
use notify::{
    EventKind, Watcher,
    event::{DataChange, ModifyKind},
};

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::*;
use rand::distr::{Alphanumeric, SampleString};

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 {
    create: Symbol<FnCreate>,
    init: Symbol<FnInit>,
    update: Symbol<FnUpdate>,
    draw: Symbol<FnDraw>,
    cleanup: Symbol<FnCleanup>,
    #[allow(dead_code)]
    lib: Library,
}

impl SymbolTable {
    pub fn load(lib_path: &PathBuf) -> Result<Self> {
        unsafe {
            #[cfg(target_family = "unix")]
            let lib: Library =
                { ::libloading::os::unix::Library::open(Some(lib_path), 0x2 | 0x1000)?.into() };
            #[cfg(not(target_family = "unix"))]
            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 struct HotReloader {
    lib_path: PathBuf,
    build_args: Option<Vec<String>>,
    table: Option<SymbolTable>,
    game: *mut std::ffi::c_void,
    watcher_rx: Receiver<notify::Result<notify::Event>>,
    #[allow(dead_code)]
    watcher: notify::RecommendedWatcher,
    compile_task_tx: Sender<CompileTask>,
    compile_res_rx: Receiver<CompileResult>,
}

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().with_poll_interval(Duration::from_secs(1)),
        )?;

        // compiler thread
        let (compile_task_tx, compile_task_rx) = std::sync::mpsc::channel();
        let (compile_res_tx, compile_res_rx) = std::sync::mpsc::channel();
        std::thread::spawn(move || {
            loop {
                let task_res: Result<CompileTask, _> = compile_task_rx.recv();
                if let Ok(CompileTask {
                    build_args,
                    lib_path,
                }) = task_res
                {
                    if let Err(err) = compile(build_args) {
                        log::error!("Compile failed {err:?}");
                    }
                    let patched_path = patch_library(&lib_path).expect("patch library");

                    let _ = compile_res_tx.send(CompileResult { patched_path });
                }
            }
        });

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

        let game = std::ptr::null_mut();
        Ok(Self {
            lib_path,
            build_args: None,
            table: None,
            game,
            watcher_rx: rx,
            watcher,
            compile_task_tx,
            compile_res_rx,
        })
    }

    pub fn set_build_args(&mut self, build_args: Vec<String>) {
        self.build_args = Some(build_args);
    }

    pub fn reload(&mut self, patched_path: &PathBuf) -> Result<()> {
        let now = Instant::now();
        let new_table = SymbolTable::load(patched_path)?;
        self.table.replace(new_table);
        log::info!(
            "Reload {:?} tooks {}ms",
            &patched_path,
            now.elapsed().as_millis()
        );
        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) => {
                    if matches!(
                        event.kind,
                        EventKind::Modify(ModifyKind::Data(DataChange::Content))
                            | EventKind::Modify(ModifyKind::Data(DataChange::Any))
                    ) {
                        log::info!("File changes: {:?}", &event.paths);
                        changed = true;
                    }
                }
                Err(error) => log::error!("Error: {error:?}"),
            }
        }

        if changed {
            self.compile_task_tx
                .send(CompileTask {
                    build_args: self.build_args.clone(),
                    lib_path: self.lib_path.clone(),
                })
                .expect("send compile task");
        }

        if let Ok(CompileResult { patched_path }) = self.compile_res_rx.try_recv()
            && let Err(err) = self.reload(&patched_path)
        {
            log::error!("Reload failed {err:?}");
        }
    }
}

impl Scene for HotReloader {
    fn init(&mut self, g: &mut Engine) {
        // do initial compile
        let now = Instant::now();
        compile(self.build_args.clone()).expect("compile");
        let patched_path = patch_library(&self.lib_path).expect("patch");
        self.reload(&patched_path).expect("reload");
        log::info!("First compile tooks {}ms", now.elapsed().as_millis());

        unsafe {
            // game create
            if let Some(table) = self.table.as_ref() {
                self.game = (*table.create)();
            }

            // game init
            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);
            }
        }
    }
}

struct CompileTask {
    build_args: Option<Vec<String>>,
    lib_path: PathBuf,
}

struct CompileResult {
    patched_path: PathBuf,
}

fn compile(build_args: Option<Vec<String>>) -> Result<()> {
    let now = Instant::now();
    let mut cmd_builder = std::process::Command::new("cargo");
    let output = match build_args.clone() {
        Some(args) => cmd_builder.args(args).output()?,
        None => cmd_builder.args(["build", "--lib"]).output()?,
    };
    if !output.status.success() {
        let err = String::from_utf8(output.stderr)?;
        log::error!("Failed to compile \n{err}");
    }
    log::info!("Compile tooks {}ms", now.elapsed().as_millis());
    Ok(())
}

pub fn patch_library(lib_path: &Path) -> Result<PathBuf> {
    let stem = lib_path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("lib");
    let ext = lib_path
        .extension()
        .and_then(|s| s.to_str())
        .unwrap_or("so");

    let random_id = Alphanumeric.sample_string(&mut rand::rng(), 8);
    let new_name = format!("{stem}-{random_id}");
    let mut new_path = lib_path
        .parent()
        .unwrap_or_else(|| Path::new("."))
        .join(new_name);
    new_path.set_extension(ext);

    fs::copy(lib_path, &new_path)?;
    Ok(new_path)
}