rksm / hot-lib-reloader-rs

Reload Rust code without app restarts. For faster feedback cycles.
MIT License
597 stars 19 forks source link

My own approach to hot-reloading. #5

Closed codecnotsupported closed 2 years ago

codecnotsupported commented 2 years ago

By coincidence I made my own hot-reloading test just a few weeks ago when I read fasterthanli's article However I find that, my setup. Is a tad bit less of a hassle to set-up. I also watch for file changes & use libloading. However I use: cargo-watch -x "build -Z unstable-options --out-dir ./plugins" Then simply watch the directory for Event::Write events.

I only need to specify the plugin directory, which I find less of a hassle to write compared to

hot_lib_reloader::define_lib_reloader! {
    unsafe MyLibLoader {
        lib_name: "lib",
        source_files: ["path/to/lib.rs"]
    }
}

For each library.

However you need to check the function signature in the source file, and must therefore know the source location. Except the source location embedded in the library if it's a debug build so you may extract it from there. I'm sure there's a library that can extract it easily, but I wouldn't the name on top of my head. (maybe symbolic-debuginfo?)

If you're interested, here's my main method for my reload test. ( Probably adopted from fasterthanli, I can't remember )

use std::{error::Error, path::Path};

// use hotwatch::{Event, Hotwatch}; // Non blocking
use hotwatch::blocking::Hotwatch;   //     Blocking
use libloading::{Library, Symbol};

static PLUGINS_LIBRARY_PATH: &str = "."; // Relative to the process' current working directory
fn main() -> Result<(), Box<dyn Error>> {
    let mut hotwatch = Hotwatch::new().expect("hotwatch failed to initialize!");

    hotwatch.watch(PLUGINS_LIBRARY_PATH, |event| {
        if let hotwatch::Event::Write(path) = event {
            if path.extension() == Some("so".as_ref()) {load_and_print(&path).expect("Error loading so file.");}
        }

        hotwatch::blocking::Flow::Continue
    })?;
    hotwatch.run();
    Ok(())
}

fn load_and_print(path: &Path) -> Result<(), libloading::Error> {
    unsafe {
        let lib = Library::new(path)?;
        let greet: Symbol<unsafe extern "C" fn(name: String)> = lib.get(b"greet")?;
        greet("reloading".to_string());
    }

    Ok(())
}
rksm commented 2 years ago

Hi and thanks for sharing! The idea with getting the file name from library is great, I'll look into that.

As for the define_lib_reloader! macro, it is actually optional to use, also source_files: ["path/to/lib.rs"] is optional.

One way to specify the signatures directly is to do:

hot_lib_reloader::define_lib_reloader! {
    unsafe MyLibLoader {
        lib_name: "lib",
        functions: {
            fn test(arg1: &str, arg2: u8) -> String;
        },
    }
}

And you can also use it without any macro:

let loader = hot_lib_reloader::LibReloader::new("target/debug", "lib").unwrap();

unsafe {
    let f = loader
        .get_symbol::<fn(&str, u8) -> String>(b"test\x00")
        .expect("Cannot load library function test");
    f(arg1, arg2)
}

would be the equivalent.

And I actually agree that the macro-free variant might even be favorable when having only few interfaces to work with. learned from working with the macro-free variant, however, was that in non-trival use cases having the signatures repeated is a problem. Here are for example the function signatures used in the bevy project I used for the demo gif:

pub fn player_movement_system(
    keyboard_input: Res<Input<KeyCode>>,
    mut query: Query<(&Player, &mut Transform)>,
    time: Res<Time>,
) {/*...*/}

pub fn player_shooting_system(
    mut commands: Commands,
    keyboard_input: Res<Input<KeyCode>>,
    query: Query<&Transform, With<Player>>,
) {/*...*/}

pub fn bullet_movement_system(
    mut commands: Commands,
    mut query: Query<(Entity, &mut Transform), With<Bullet>>,
    cam: Query<&Camera>,
    time: Res<Time>,
) {/*...*/}

pub fn bullet_hit_system(
    mut commands: Commands,
    bullet_query: Query<&Transform, With<Bullet>>,
    ship_query: Query<(Entity, &Transform), With<OtherShip>>,
) {/*...*/}

pub fn spawn_other_ships(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    others: Query<(Entity, &Transform), With<OtherShip>>,
    cam: Query<&Camera>,
) {/*...*/}

pub fn move_other_ships(
    time: Res<Time>,
    mut query: Query<&mut Transform, With<OtherShip>>,
) {/*...*/}

I found that while I was working on it that I would need to change the parameters quite a bit (which of course takes a full restart). So much so that it became slower to work with the hot-reload version than without. To address this issue I came up with the macro that extracts those types automatically which (from my personal point of view) provides quite an efficiency boost.