hot-lib-reloader-rs

repository·master·Indexed 20 days ago

https://github.com/rksm/hot-lib-reloader-rs

A Rust development tool that enables live programming by allowing functions of a running program to be reloaded without restarting. It works by loading code as a dynamic library (dylib) and using macros to wrap exported functions. The library provides utilities like the #[hot_lib_reloader::hot_module] macro, LibReloadObserver for handling reload events, and the #[no_mangle_if_debug] macro for conditional function exposure.

Tokens
10.1K
Snippets
33
Records
42
Agent score
73%

What's inside hot-lib-reloader

  1. Understand the limitations of hot-reloading

    master

    Hot-reloading Rust code via dynamic libraries has several critical constraints that can lead to crashes if not managed:

    • No signature changes: Changing the parameter or return types of a hot-reloadable function will cause a crash because the executable's expectations will no longer match the library's implementation.
    • Type changes require care: Structs and enums shared between the executable and the library cannot have their memory layout changed freely. Differing layouts cause undefined behavior and crashes.
    • No generics: Functions marked for hot-reloading cannot be generic because #[unsafe(no_mangle)] does not support them.
    • Global state issues: If your library contains global state (or depends on a crate that does), it must be re-initialized after a reload. Additionally, crates relying on TypeId (like many ECS systems) may fail because types have different IDs after a reload.

    For a detailed discussion on these caveats, refer to the official blog post on hot-reloading Rust.

  2. Avoid breaking component type IDs in Bevy

    master

    When using hot-reloading with Bevy, changing code in the same crate that defines your components can change their Type IDs. This causes component queries to suddenly return empty results after a reload.

    Best Practice: Define your components and state in a separate sub-crate (e.g., a components crate) that is independent of the reloadable systems crate. This ensures they remain a separate compilation unit and preserves stable Type IDs.

  3. Use serialization to modify types and state freely during hot-reloading

    master

    When using hot-lib-reloader, changing the structure of your data types can cause reloading to fail because the state held by the executable no longer matches the new type definition in the library.

    To bypass this limitation, you can use a pattern where the executable maintains an outer container that holds the application state as a serde_json::Value. This allows you to modify the internal types and state structure freely without breaking the reload mechanism, as the outer container's type remains constant.

    // Conceptual pattern: Use serde_json::Value for the inner state
    struct AppState {
        inner: serde_json::Value,
    }
  4. Run the Bevy example with hot-reloading

    master

    To enable hot-reloading in the Bevy example, you must run two commands in parallel. One command watches the source crates (systems and components) and rebuilds the dynamic library when changes occur, while the other runs the main application with the reload feature enabled.

    Linux and macOS

    Use the following commands:

    $ cargo watch -w systems -w components -x "build -p systems --features dynamic"
    $ cargo run --features reload

    Windows

    On Windows, you must use a separate target directory for the executable to avoid file locking issues caused by bevy_dylib.dll.

    $ cargo watch -w systems -w components -x "build -p systems --features dynamic"
    $ cargo run --features reload --target-dir "target-bin"
    # Linux/macOS
    $ cargo watch -w systems -w components -x "build -p systems --features dynamic"
    $ cargo run --features reload
    
    # Windows
    $ cargo watch -w systems -w components -x "build -p systems --features dynamic"
    $ cargo run --features reload --target-dir "target-bin"
  5. Use hot-lib-reloader with iced

    master

    This example demonstrates how to integrate hot-lib-reloader with the iced GUI framework.

    Platform Limitation: This specific implementation currently only works on macOS. For details on cross-platform support, refer to the project's issue tracker.

    Quickstart Workflow

    1. Run the application: Use the just command runner to start the project.
      just run
    2. Modify logic: Open lib/src/lib.rs and modify the update function (for example, by adding a println!() statement).
    3. Observe reload: The library will detect the change, reload the dynamic library, and the new logic will immediately take effect in the running application.
    just run
  6. Use no-mangle-if-debug to conditionally expose functions for hot-reloading

    master

    The #[no_mangle_if_debug] macro is used to conditionally apply #[unsafe(no_mangle)] to an item, but only when compiled in debug mode. This is specifically useful when working with hot-lib-reloader to ensure library functions are exposed to the reloader during development without incurring the no_mangle penalty in release builds.

    When applied, the macro expands to:

    • #[cfg(debug_assertions)] #[unsafe(no_mangle)] for the item in debug mode.
    • #[cfg(not(debug_assertions))] for the item in release mode (making it a standard function).
    #[no_mangle_if_debug]
    fn func() {}
  7. Run the minimal hot reload setup

    master

    To achieve the simplest hot reload configuration, use cargo watch to monitor the library crate and trigger builds and runs automatically. You need two separate cargo watch processes running in parallel: one to watch for changes in the library and rebuild it, and another to watch for changes and restart the application.

    Note: This setup assumes your library is in a package named lib and your main application is in a package that can be run via cargo run.

    $ cargo watch -w lib -x 'build -p lib'
    $ cargo watch -i lib -x run
  8. Configure hot reloading using feature flags

    master

    You can make hot reloading optional in your project by wrapping the hot-reloading logic behind a Cargo feature (e.g., reload). This allows you to switch between a hot-reloading development environment and a standard statically compiled binary without changing your source code structure.

    Development Workflow

    To use hot reloading, run the binary with the reload feature enabled in one terminal, and use a separate terminal to watch and rebuild the library crate whenever it changes.

    1. Start the reloader binary:

      cargo watch -i lib -x 'run --features reload'
    2. Watch and rebuild the library:

      cargo watch -w lib -x 'build -p lib'

    Production/Static Workflow

    To run the application as a standard, statically compiled binary (without the reloader), simply use the standard run command:

    cargo run
    # Terminal 1: Run the binary with the reload feature
    cargo watch -i lib -x 'run --features reload'
    
    # Terminal 2: Rebuild the library crate on change
    cargo watch -w lib -x 'build -p lib'
    
    # Standard static run
    cargo run
  9. Set up a hot-reloadable workspace

    master

    To use hot-lib-reloader, you must structure your project as a workspace where the hot-reloadable code resides in a library (dylib) and the main application resides in an executable.

    1. Library Configuration

    In your library's Cargo.toml (e.g., ./lib/Cargo.toml), set the crate-type to include dylib:

    [package]
    name = "lib"
    version = "0.1.0"
    edition = "2024"
    
    [lib]
    crate-type = ["rlib", "dylib"]

    Functions intended for hot-reloading must be pub and annotated with #[unsafe(no_mangle)].

    #[unsafe(no_mangle)]
    pub fn step(state: &mut State) {
        // ...
    }

    2. Executable Configuration

    In your main executable's Cargo.toml (e.g., ./Cargo.toml), add hot-lib-reloader and your library as dependencies:

    [workspace]
    resolver = "2"
    members = ["lib"]
    
    [package]
    name = "bin"
    version = "0.1.0"
    edition = "2024"
    
    [dependencies]
    hot-lib-reloader = "0.8"
    lib = { path = "lib" }

    3. Integrating the Hot Module

    In your main.rs, use the #[hot_lib_reloader::hot_module] macro to wrap the library functions. You must also use the hot_functions_from_file! macro to point to the library's source file.

    #[hot_lib_reloader::hot_module(dylib = "lib")]
    mod hot_lib {
        // Path relative to project root
        hot_functions_from_file!("lib/src/lib.rs");
    
        // Re-export types used in function signatures
        pub use lib::State;
    }
    [lib]
    crate-type = ["rlib", "dylib"]
  10. Run the simplest hot reload setup

    master

    To achieve a basic hot reload workflow, you need to run two concurrent processes: one to watch the library for changes and rebuild it, and another to watch the library for changes and restart the runner. Use cargo watch to monitor the lib directory.

    1. Rebuild the library on change: cargo watch -w lib -x 'build -p lib'

    2. Restart the application on change: cargo watch -i lib -x run

    $ cargo watch -w lib -x 'build -p lib'
    $ cargo watch -i lib -x run
  11. Run the nannou-vector-field hot-reload example

    master

    To run the nannou-vector-field example with hot-reloading enabled, you can use either the just runner or a combination of cargo run and cargo watch. The cargo watch method specifically monitors the lib directory and triggers a build for the lib package whenever changes are detected, enabling the hot-reload cycle.

    # Option 1: Using just
    just run
    
    # Option 2: Using cargo run and cargo watch
    cargo run
    cargo watch -w lib -x "build -p lib"