hot_lib_reloader/lib.rs
1#![allow(clippy::needless_doctest_main)]
2
3/*!
4
5
6`hot-lib-reloader` is a development tool that allows you to reload functions of a running Rust program.
7This allows to do "live programming" where you modify code and immediately see the effects in your running program.
8
9This is build around the [libloading crate](https://crates.io/crates/libloading) 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](https://robert.kra.hn/posts/hot-reloading-rust).
10
11For a demo and explanation see also [this Rust and Tell presentation](https://www.youtube.com/watch?v=-UUImyqX8j0).
12
13# Table of contents:
14
15- [Usage](#usage)
16 - [Example project setup](#example-project-setup)
17 - [Executable](#executable)
18 - [Library](#library)
19 - [Running it](#running-it)
20 - [lib-reload events](#lib-reload-events)
21
22- [Usage tips](#usage-tips)
23 - [Know the limitations](#know-the-limitations)
24 - [No signature changes](#no-signature-changes)
25 - [Type changes require some care](#type-changes-require-some-care)
26 - [Hot-reloadable functions cannot be generic](#hot-reloadable-functions-cannot-be-generic)
27 - [Global state in reloadable code](#global-state-in-reloadable-code)
28 - [Use feature flags to switch between hot-reload and static code](#use-feature-flags-to-switch-between-hot-reload-and-static-code)
29 - [Disable `#[no-mangle]` in release mode](#disable-no-mangle-in-release-mode)
30 - [Use serialization or generic values for changing types](#use-serialization-or-generic-values-for-changing-types)
31 - [Use a hot-reload friendly app structure](#use-a-hot-reload-friendly-app-structure)
32 - [Use multiple libraries](#use-multiple-libraries)
33 - [Adjust the file watch debounce duration](#adjust-the-file-watch-debounce-duration)
34 - [Debugging](#debugging)
35
36- [Examples](#examples)
37
38- [Known issues](#known-issues)
39 - [tracing crate](#tracing-crate)
40
41
42# Usage
43
44To quicky generate a new project supporting hot-reload you can use a [cargo generate](https://cargo-generate.github.io/cargo-generate/) template: `cargo generate rksm/rust-hot-reload`.
45
46
47## Prerequisites
48
49### macOS
50On macOS the reloadable library needs to get codesigned.
51For this purpose, hot-lib-reloader will try to use the `codesign` binary that is part of the XCode command line tools.
52It is recommended to make sure [those are installed](https://mac.install.guide/commandlinetools/).
53
54### Other platforms
55It should work out of the box.
56
57
58## Example project setup
59
60Assuming you use a workspace project with the following layout:
61
62```output
63├── Cargo.toml
64└── src
65│ └── main.rs
66└── lib
67 ├── Cargo.toml
68 └── src
69 └── lib.rs
70```
71
72
73### Executable
74
75Setup the workspace with a root project named `bin` in `./Cargo.toml`:
76
77```toml
78[workspace]
79resolver = "2"
80members = ["lib"]
81
82[package]
83name = "bin"
84version = "0.1.0"
85edition = "2024"
86
87[dependencies]
88hot-lib-reloader = "0.8"
89lib = { path = "lib" }
90```
91
92In `./src/main.rs` define a sub-module using the
93[`hot_lib_reloader_macro::hot_module`] attribute macro which wraps the functions
94exported by the library:
95
96```ignore
97// The value of `dylib = "..."` should be the library containing the hot-reloadable functions
98// It should normally be the crate name of your sub-crate.
99#[hot_lib_reloader::hot_module(dylib = "lib")]
100mod hot_lib {
101 // Reads public no_mangle functions from lib.rs and generates hot-reloadable
102 // wrapper functions with the same signature inside this module.
103 // Note that this path relative to the project root (or absolute)
104 hot_functions_from_file!("lib/src/lib.rs");
105
106 // Because we generate functions with the exact same signatures,
107 // we need to import types used
108 pub use lib::State;
109}
110
111fn main() {
112 let mut state = hot_lib::State { counter: 0 };
113 // Running in a loop so you can modify the code and see the effects
114 loop {
115 hot_lib::step(&mut state);
116 std::thread::sleep(std::time::Duration::from_secs(1));
117 }
118}
119```
120
121### Library
122
123The library should expose functions. It should set the crate type `dylib` in `./lib/Cargo.toml`:
124
125```toml
126[package]
127name = "lib"
128version = "0.1.0"
129edition = "2024"
130
131[lib]
132crate-type = ["rlib", "dylib"]
133```
134
135The 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.
136
137```
138pub struct State {
139 pub counter: usize,
140}
141
142#[unsafe(no_mangle)]
143pub fn step(state: &mut State) {
144 state.counter += 1;
145 println!("doing stuff in iteration {}", state.counter);
146}
147```
148
149### Running it
150
1511. Start compilation of the library: `cargo watch -w lib -x 'build -p lib'`
1522. In another terminal run the executable: `cargo run`
153
154Now change for example the print statement in `lib/lib.rs` and see the effect on the runtime.
155
156
157In addition, using a tool like [gnu parallel](https://www.gnu.org/software/parallel/) or [concurrently](https://github.com/open-cli-tools/concurrently) is recommended. This allows to run both the lib build and the application in one go.
158
159Example:
160
161```bash
162# Forwards output, stops all on ctr-c, fails if one command fails
163parallel --line-buffer --halt now,fail=1 ::: \
164 "cargo watch -i lib -x run" \
165 "cargo watch -w lib -x 'build -p lib'"
166
167```
168
169
170
171## lib-reload events
172
173### LibReloadObserver
174
175You can get notified about two kinds of events using the methods provided by [`LibReloadObserver`]:
176
177- [`wait_for_about_to_reload`](LibReloadObserver::wait_for_about_to_reload) the watched library is about to be reloaded (but the old version is still loaded)
178- [`wait_for_reload`](LibReloadObserver::wait_for_reload) a new version of the watched library was just reloaded
179
180This 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.
181
182To 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.
183In 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):
184
185```ignore
186#[hot_lib_reloader::hot_module(dylib = "lib")]
187mod hot_lib {
188 /* code from above */
189
190 // expose a type to subscribe to lib load events
191 #[lib_change_subscription]
192 pub fn subscribe() -> hot_lib_reloader::LibReloadObserver {}
193}
194```
195
196And then the main function just waits for reloaded events:
197
198```ignore
199fn main() {
200 let mut state = hot_lib::State { counter: 0 };
201 let lib_observer = hot_lib::subscribe();
202 loop {
203 hot_lib::step(&mut state);
204 // blocks until lib was reloaded
205 lib_observer.wait_for_reload();
206 }
207}
208```
209
210How to block reload to do serialization / deserialization is shown in the [reload-events example](https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/reload-events).
211
212
213### `was_updated` flag
214
215To just figure out if the library has changed, a simple test function can be exposed:
216
217```ignore
218#[hot_lib_reloader::hot_module(dylib = "lib")]
219mod hot_lib {
220 /* ... */
221 #[lib_updated]
222 pub fn was_updated() -> bool {}
223}
224```
225
226`hot_lib::was_updated()` will return `true` the first time it is called after the library was reloaded.
227It will then return false until another reload occurred.
228
229
230
231# Usage tips
232
233
234## Know the limitations
235
236Reloading code from dynamic libraries comes with a number of caveats which are discussed in some detail [here](https://robert.kra.hn/posts/hot-reloading-rust/#caveats-and-asterisks).
237
238
239### No signature changes
240
241When 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.
242
243
244### Type changes require some care
245
246Types 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.
247
248See [use serialization](#use-serialization-or-generic-values-for-changing-types) for a way around it.
249
250
251### Hot-reloadable functions cannot be generic
252
253Since `#[unsafe(no_mangle)]` does not support generics, generic functions can't be named / found in the library.
254
255### Global state in reloadable code
256
257If 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.
258
259Note also that "global state" is more than just global variables. As noted in [this issue](https://github.com/rksm/hot-lib-reloader-rs/issues/34), crates relying on the [TypeId](https://doc.rust-lang.org/std/any/struct.TypeId.html) 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.
260
261
262## Use feature flags to switch between hot-reload and static code
263
264See the [reload-feature example](https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/reload-feature) for a complete project.
265
266Cargo allows to specify optional dependencies and conditional compilation through feature flags.
267When you define a feature like this
268
269```toml
270[features]
271default = []
272reload = ["lib/reload", "dep:hot-lib-reloader"]
273
274[dependencies]
275lib = { path = "lib" }
276hot-lib-reloader = { version = "^0.6", optional = true }
277```
278
279and 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:
280
281```ignore
282#[cfg(feature = "reload")]
283use hot_lib::*;
284#[cfg(not(feature = "reload"))]
285use lib::*;
286
287#[cfg(feature = "reload")]
288#[hot_lib_reloader::hot_module(dylib = "lib")]
289mod hot_lib { /*...*/ }
290```
291
292To run the static version just use `cargo run` the hot reloadable variant with `cargo run --features reload`.
293
294
295### Disable `#[no-mangle]` in release mode
296
297To 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:
298
299#### With a feature flag
300
301Conditionally use `#[no_mangle]` in your library:
302
303```rust
304#[cfg_attr(feature = "reload", unsafe(no_mangle))]
305```
306
307To run the static version just use `cargo run` the hot reloadable variant with `cargo run --features reload`.
308
309#### Using `no-mangle-if-debug` macro
310
311Use the [no-mangle-if-debug attribute macro](./macro-no-mangle-if-debug). It will conditionally disable name mangling, depending on wether you build release or debug mode.
312
313
314## Use serialization or generic values for changing types
315
316If 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](https://docs.rs/serde_json/latest/serde_json/value/enum.Value.html), you don't need string or binary formats and typically don't even need to clone anything.
317
318Here is an example where we crate a state container that has an inner `serde_json::Value`:
319
320```ignore
321#[hot_lib_reloader::hot_module(dylib = "lib")]
322mod hot_lib {
323 pub use lib::State;
324 hot_functions_from_file!("lib/src/lib.rs");
325}
326
327fn main() {
328 let mut state = hot_lib::State {
329 inner: serde_json::json!(null),
330 };
331
332 loop {
333 state = hot_lib::step(state);
334 std::thread::sleep(std::time::Duration::from_secs(1));
335 }
336}
337```
338
339In the library we are now able to change the value and type layout of `InnerState` as we wish:
340
341
342```ignore
343#[derive(Debug)]
344pub struct State {
345 pub inner: serde_json::Value,
346}
347
348#[derive(serde::Deserialize, serde::Serialize)]
349struct InnerState {}
350
351#[unsafe(no_mangle)]
352pub fn step(state: State) -> State {
353 let inner: InnerState = serde_json::from_value(state.inner).unwrap_or(InnerState {});
354
355 // You can modify the InnerState layout freely and state.inner value here freely!
356
357 State {
358 inner: serde_json::to_value(inner).unwrap(),
359 }
360}
361```
362
363
364Alternatively 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](https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/reload-events).
365
366
367
368## Use a hot-reload friendly app structure
369
370Whether or not hot-reload is easy to use depends on how you architect your app. In particular, the ["functional core, imparative shell" pattern](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell) makes it easy to split state and behavior and works well with `hot-lib-reloader`
371
372For 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.
373
374But even when using a framework that takes control, chances are that there are ways to have it call hot-reloadable code. The [bevy example](https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/bevy) where system functions can be made hot-reloadable, shows how this can work.
375See the [egui](https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/hot-egui) and [tokio](https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/reload-events) examples possible setupts.
376
377
378## Adjust the file watch debounce duration
379
380The `hot_module` macro allows setting the `file_watch_debounce` attribute which defines the debounce duration for file changes in milliseconds.
381This is 500ms by default.
382If you see multiple updates triggered for one recompile (can happen the library is very large), increase that value.
383You can try to decrease it for faster reloads. With small libraries / fast hardware 50ms or 20ms should work fine.
384
385```ignore
386#[hot_module(dylib = "lib", file_watch_debounce = 50)]
387/* ... */
388```
389
390## Change the name and location of the dylib file
391
392By default `hot-lib-reloader` assumes that there will be a dynamic library available in the `$CARGO_MANIFEST_DIR/target/debug/` or `$CARGO_MANIFEST_DIR/target/release` folder, depending on whether the debug or release profile is used.
393The name of the library is defined by the `dylib = "..."` portion of the `#[hot_module(...)]` macro.
394So by specifying `#[hot_module(dylib = "lib")]` and building with debug settings, `hot-lib-reloader` will try to load a `target/debug/liblib.dylib` on MacOS, a `target/debug/liblib.so` on Linux or a `target/debug/lib.dll` on Windows.
395
396If the library should be loaded from a different location you can specify this by setting the `lib_dir` attribute like:
397
398```ignore
399#[hot_lib_reloader::hot_module(
400 dylib = "lib",
401 lib_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/target/debug")
402)]
403mod hot_lib {
404 /* ... */
405}
406```
407
408### Adjust the dylib filename
409
410The `hot_module` macro allows setting the shadow file name using the `loaded_lib_name_template` parameter.
411This is useful when multiple processes are trying to hot reload the same library and can be used to prevent conflicts.
412This attribute allows for placeholders that can be dynamically replaced:
413
414| Placeholder | Description | Feature Flag |
415|-------------------|----------------------------------------------------|---------------|
416| `{lib_name}` | Name of the library as defined in your code | None |
417| `{load_counter}` | Incremental counter for each hot reload | None |
418| `{pid}` | Process ID of the running application | None |
419| `{uuid}` | A UUID v4 string | `uuid` |
420
421If you don't specify the `loaded_lib_name_template` parameter, a default naming convention is used for the shadow filename.
422This default pattern is: `{lib_name}-hot-{load_counter}`.
423
424```rust
425#[hot_lib_reloader::hot_module(
426 dylib = "lib",
427 // Might result in the following shadow file lib_hot_2644_0_5e659d6e-b78c-4682-9cdd-b8a0cd3e8fc6.dll
428 // Requires the 'uuid' feature flags for the {uuid} placeholder
429 loaded_lib_name_template = "{lib_name}_hot_{pid}_{load_counter}_{uuid}"
430)]
431mod hot_lib {
432 /* ... */
433}
434```
435
436
437## Debugging
438
439If your `hot_module` gives you a strange compilation error, try `cargo expand` to see what code is generated.
440
441By default the `hot-lib-reloader` crate won't write to stdout or stderr but it logs what it does with info, debug, and trace log levels using the [log crate](https://crates.io/crates/log).
442Depending on what logging framework you use (e.g. [env_logger](https://crates.io/crates/env_logger)), you can enable those logs by setting a `RUST_LOG` filter like `RUST_LOG=hot_lib_reloader=trace`.
443
444
445# Examples
446
447Examples can be found at [rksm/hot-lib-reloader-rs/examples](https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples).
448
449- [minimal](https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/minimal): Bare-bones setup.
450- [reload-feature](https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/reload-feature): Use a feature to switch between dynamic and static version.
451- [serialized-state](https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/serialized-state): Shows an option to allow to modify types and state freely.
452- [reload-events](https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/reload-events): How to block reload to do serialization / deserialization.
453- [all-options](https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/all-options): All options the `hot_module` macro accepts.
454- [bevy](https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/bevy): Shows how to hot-reload bevy systems.
455- [nannou](https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/nannou-vector-field): Interactive generative art with [nannou](https://nannou.cc).
456- [egui](https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/hot-egui): How to hot-reload a native egui / eframe app.
457- [iced](https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/hot-iced): How to hot-reload an iced app.
458
459
460
461# Known issues
462
463## tracing crate
464
465When used with the `tracing` crate multiple issues can occur:
466- When `tracing` is used in the library that is reloaded the app sometimes crashes with `Attempted to register a DefaultCallsite that already exists!`
467- When used in combination with bevy, `commands.insert(component)` operations stop to work after a reload, likely because of internal state getting messed up.
468
469If you can, don't use `hot-lib-reloader` in combination with `tracing`.
470
471
472
473# License
474
475[MIT](https://github.com/rksm/hot-lib-reloader-rs/blob/hot-module/LICENSE)
476
477*/
478
479mod error;
480mod lib_reload_events;
481mod lib_reloader;
482
483#[cfg(feature = "verbose")]
484mod log;
485
486#[cfg(target_os = "macos")]
487mod codesign;
488
489pub use error::HotReloaderError;
490pub use hot_lib_reloader_macro::hot_module;
491pub use lib_reload_events::{BlockReload, ChangedEvent, LibReloadNotifier, LibReloadObserver};
492pub use lib_reloader::LibReloader;