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)),
)?;
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 });
}
}
});
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(())
}
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) {
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 {
if let Some(table) = self.table.as_ref() {
self.game = (*table.create)();
}
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)
}