Reload Rust code without app restarts. For faster feedback cycles.
Reload Rust code without app restarts. For faster feedback cycles.
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.
For a demo and explanation see also this Rust and Tell presentation.
To quicky generate a new project supporting hot-reload you can use a cargo generate template: cargo generate rksm/rust-hot-reload.
On macOS the reloadable library needs to get codesigned.
For this purpose, hot-lib-reloader will try to use the codesign binary that is part of the XCode command line tools.
It is recommended to make sure those are installed.
It should work out of the box.
Assuming you use a workspace project with the following layout:
├── Cargo.toml
└── src
│ └── main.rs
└── lib
├── Cargo.toml
└── src
└── lib.rs
Setup the workspace with a root project named bin in ./Cargo.toml:
[workspace]
resolver = "2"
members = ["lib"]
[package]
name = "bin"
version = "0.1.0"
edition = "2024"
[dependencies]
hot-lib-reloader = "0.8"
lib = { path = "lib" }
In ./src/main.rs define a sub-module using the
[hot_lib_reloader_macro::hot_module] attribute macro which wraps the functions
exported by the library:
…
The library should expose functions. It should set the crate type dylib in ./lib/Cargo.toml:
[package]
name = "lib"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["rlib", "dylib"]
The functions you want to be reloadable should be public and have the #[unsafe(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.
pub struct State {
pub counter: usize,
}
#[unsafe(no_mangle)]
pub fn step(state: &mut State) {
state.counter += 1;
println!("doing stuff in iteration {}", state.counter);
}
cargo watch -w lib -x 'build -p lib'cargo runNow change for example the print statement in lib/lib.rs and see the effect on the runtime.
In addition, using a tool like gnu parallel or concurrently is recommended. This allows to run both the lib build and the application in one go.
Example:
# Forwards output, stops all on ctr-c, fails if one command fails
parallel --line-buffer --halt now,fail=1 ::: \
"cargo watch -i lib -x run" \
"cargo watch -w lib -x 'build -p lib'"
You can get notified about two kinds of events using the methods provided by [LibReloadObserver]:
wait_for_about_to_reload the watched library is about to be reloaded (but the old version is still loaded)wait_for_reload a new version of the watched library was just reloadedThis is useful to run code before and / or after library updates. One use case is to serialize and then deserialize state another one is driving the application.
To continue with the example above, let's say instead of running the library function step every second we only want to re-run it when the library has changed.
In order to do that, we first need to get hold of the LibReloadObserver. For that we can expose a function subscribe() that is annotated with the #[lib_change_subscription] (that attribute tells the hot_module macro to provide an implementation for it):
#[hot_lib_reloader::hot_module(dylib = "lib")]
mod hot_lib {
/* code from above */
// expose a type to subscribe to lib load events
#[lib_change_subscription]
pub fn subscribe() -> hot_lib_reloader::LibReloadObserver {}
}
And then the main function just waits for reloaded events:
fn main() {
let mut state = hot_lib::State { counter: 0 };
let lib_observer = hot_lib::subscribe();
loop {
hot_lib::step(&mut state);
// blocks until lib was reloaded
lib_observer.wait_for_reload();
}
}
How to block reload to do serialization / deserialization is shown in the reload-events example.
was_updated flagTo just figure out if the library has changed, a simple test function can be exposed:
#[hot_lib_reloader::hot_module(dylib = "lib")]
mod hot_lib {
/* ... */
#[lib_updated]
pub fn was_updated() -> bool {}
}
hot_lib::was_updated() will return true the first time it is called after the library was reloaded.
It will then return false until another reload occurred.
Reloading code from dynamic libraries comes with a number of caveats which are discussed in some detail here.
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.
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.
Since #[unsafe(no_mangle)] does not support generics, generic functions can't be named / found in the library.
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.
Note also that "global state" is more than just global variables. As noted in this issue, crates relying on the TypeId of a type (like most ECS systems do) will expect the type/id mapping to be constant. After reloading, types will have different ids, however, which makes (de)serialization more challenging.
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
[features]
default = []
reload = ["lib/reload", "dep:hot-lib-reloader"]
[dependencies]
lib = { path = "lib" }
hot-lib-reloader = { version = "^0.6", 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:
#[cfg(feature = "reload")]
use hot_lib::*;
#[cfg(not(feature = "reload"))]
use lib::*;
#[cfg(feature = "reload")]
#[hot_lib_reloader::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.
#[no-mangle] in release modeTo not pay a penalty for exposing functions using #[unsafe(no_mangle)] in release mode where everything is statically compiled (see previous tip) and no functions need to be exported, there are two options:
Conditionally use #[no_mangle] in your library:
#[cfg_attr(feature = "reload", unsafe(no_mangle))]
To run the static version just use cargo run the hot reloadable variant with cargo run --features reload.
no-mangle-if-debug macroUse the no-mangle-if-debug attribute macro. It will conditionally disable name mangling, depending on wether you build release or debug mode.
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:
#[hot_lib_reloader::hot_module(dylib = "lib")]
mod hot_lib {
pub use lib::State;
hot_functions_from_file!("lib/src/lib.rs");
}
fn main() {
let mut state = hot_lib::State {
inner: serde_json::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:
#[derive(Debug)]
pub struct State {
pub inner: serde_json::Value,
}
#[derive(serde::Deserialize, serde::Serialize)]
struct InnerState {}
#[unsafe(no_mangle)]
pub fn step(state: State) -> State {
let inner: InnerState = serde_json::from_value(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(),
}
}
Alternatively you can also do the serialization just before the lib is to be reloaded and deserialize immediately thereafter. This is shown in the reload-events example.
Whether or not hot-reload is easy to use depends on how you architect y
No open issues yet, or sync has not completed.