hot-lib-reloader

Crates.io CI License

hot-lib-reloader is a development tool that allows you to reload functions of a running Rust program. This allows to do "live programming" where you modify code and immediately see the effects in your running program.

This is build around the libloading crate and will require you to put code you want to hot-reload inside a Rust library (dylib). For a detailed discussion about the idea and implementation see this blog post.

Table of contents:

Usage

To quicky generate a new project supporting hot-reload you can use a cargo generate template: cargo generate rksm/rust-hot-reload.

Example project setup

Assuming you use a workspace project with the following layout:

output ├── Cargo.toml └── src │ └── main.rs └── lib ├── Cargo.toml └── src └── lib.rs

Executable

Setup the workspace with a root project named bin in ./Cargo.toml:

```toml [workspace] resolver = "2" members = ["lib"]

[package] name = "bin" version = "0.1.0" edition = "2021"

[dependencies] hot-lib-reloader = "^0.5" lib = { path = "lib" } ```

In ./src/main.rs define a sub-module which wraps the functions exported by the library:

``rust // The value ofdylib = "..."` should be the library containing the hot-reloadable functions // It should normally be the crate name of your sub-crate.

[hotlibreloader::hot_module(dylib = "lib")]

mod hotlib { // Reads public nomangle functions from lib.rs and generates hot-reloadable // wrapper functions with the same signature inside this module. hotfunctionsfrom_file!("../lib/src/lib.rs");

// Because we generate functions with the exact same signatures,
// we need to import types used
pub use lib::State;

}

fn main() { let mut state = hotlib::State { counter: 0 }; // Running in a loop so you can modify the code and see the effects loop { hotlib::dostuff(&mut state); std::thread::sleep(std::time::Duration::fromsecs(1)); } } ```

Library

The library should expose functions. It should set the crate type dylib in ./lib/Cargo.toml:

```toml [package] name = "lib" version = "0.1.0" edition = "2021"

[lib] crate-type = ["rlib", "dylib"] ```

The functions you want to be reloadable should be public and have the #[no_mangle] attribute. Note that you can define other function that are not supposed to change without no_mangle and you will be able to use those alongside the other functions.

```rust pub struct State { pub counter: usize, }

[no_mangle]

pub fn do_stuff(state: &mut State) { state.counter += 1; println!("doing stuff in iteration {}", state.counter); } ```

Running it

  1. Start compilation of the library: cargo watch -w lib -x 'build -p lib'
  2. In another terminal run the executable: cargo run

Now change for example the print statement in lib/lib.rs and see the effect on the runtime.

In addition, using a tool like cargo runcc is recommended. This allows to run both the lib build and the application in one go.

Usage tips

Know the limitations

Reloading code from dynamic libraries comes with a number of caveats which are discussed in some detail here.

No signature changes

When the signature of a hot-reloadable function changes, the parameter and result types the executable expects differ from what the library provides. In that case you'll likely see a crash.

Type changes require some care

Types of structs and enums that are used in both the executable and library cannot be freely changed. If the layout of types differs you run into undefined behavior which will likely result in a crash.

See use serialization for a way around it.

Hot-reloadable functions cannot be generic

Since #[no_mangle] does not support generics, generic functions can't be named / found in the library.

Global state in reloadable code

If your hot-reload library contains global state (or depends on a library that does), you will need to re-initialize it after reload. This can be a problem with libraries that hide the global state from the user. If you need to use global state, keep it inside the executable and pass it into the reloadable functions if possible.

Rust nightly

You currently need to use Rust nightly to run hot-reloadable code. The reason for that is that we currently need the proc_macro::Span feature. We are looking into a solution that works on stable.

Use feature flags to switch between hot-reload and static code

See the reload-feature example for a complete project.

Cargo allows to specify optional dependencies and conditional compilation through feature flags. When you define a feature like this

```toml [features] default = [] reload = ["dep:hot-lib-reloader"]

[dependencies] hot-lib-reloader = { version = "^0.5", optional = true } ```

and then conditionally use either the normal or the hot module in the code calling the reloadable functions you can seamlessly switch between a static and hot-reloadable version of your application:

```rust

[cfg(feature = "reload")]

use hot_lib::*;

[cfg(not(feature = "reload"))]

use lib::*;

[cfg(feature = "reload")]

[hotlibreloader::hot_module(dylib = "lib")]

mod hot_lib { /.../ } ```

To run the static version just use cargo run the hot reloadable variant with cargo run --features reload.

Use serialization or generic values for changing types

If you want to iterate on state while developing you have the option to serialize it. If you use a generic value representation such as serde_json::Value, you don't need string or binary formats and typically don't even need to clone anything.

Here is an example where we crate a state container that has an inner serde_json::Value:

```rust

[hotlibreloader::hot_module(dylib = "lib")]

mod hotlib { pub use lib::State; hotfunctionsfromfile!("../lib/src/lib.rs"); }

fn main() { let mut state = hotlib::State { inner: serdejson::json!(null), };

loop {
    state = hot_lib::step(state);
    std::thread::sleep(std::time::Duration::from_secs(1));
}

} ```

In the library we are now able to change the value and type layout of InnerState as we wish:

```rust

[derive(Debug)]

pub struct State { pub inner: serde_json::Value, }

[derive(serde::Deserialize, serde::Serialize)]

struct InnerState {}

[no_mangle]

pub fn step(state: State) -> State { let inner: InnerState = serdejson::fromvalue(state.inner).unwrap_or(InnerState {});

// You can modify the InnerState layout freely and state.inner value here freely!

State {
    inner: serde_json::to_value(inner).unwrap(),
}

} ```

Use a hot-reload friendly app structure

Whether or not hot-reload is easy to use depends on how you architect your app. In particular, the "functional core, imparative shell" pattern makes it easy to split state and behavior and works well with hot-lib-reloader

For example, for a simple game where you have the main loop in your control, setting up the outer state in the main function and then passing it into a fn update(state: &mut State) and a fn render(state: &State) is a straightforward way to get two hot-reloadable functions.

But even when using a framework that takes control, chances are that there are ways to have it call hot-reloadable code. The bevy example where system functions can be made hot-reloadable, shows how this can work.

Code-completion with rust-analyzer

Functions that get injected with automatic code generation that happens with hot_functions_from_file!("path/to/file.rs"); won't be picked up by rust-analyzer and thus you don't have auto-completion for them.

There is a different syntax available that allows you to define reloadable functions inline so that they get picked up by rust-analyzer:

```rust

[hotlibreloader::hot_module(dylib = "lib")]

mod hotlib { #[hotfunctions] extern "Rust" { pub fn do_stuff(); } } ```

Debugging

If your hot_module gives you a strange compilation error, try cargo expand to see what code is generated.

Examples

Examples can be found at rksm/hot-lib-reloader-rs/examples.

Known issues

tracing crate

When used with the tracing crate multiple issues can occur: - When tracing is used in the library that is reloaded the app sometimes crashes with Attempted to register a DefaultCallsite that already exists! - When used in combination with bevy, commands.insert(component) operations stop to work after a reload, likely because of internal state getting messed up.

If you can, don't use hot-lib-reloader in combination with tracing.

License

MIT

License: MIT