Expand description
Library for hot-reloading functions in Rust.
§Usage
hotcode works only inside library crates. A typical supported project consists of a library
crate with runnable binaries, or an extra binary crate that uses your library.
First, add hotcode as a dependency to your library crate.
Then, add cdylib and lib to your library’s crate types:
# Cargo.toml
[lib]
crate-type = ["cdylib", "lib"]Finally, apply #[hotreload] attribute to the function that you want to reload:
// source code
#[hotcode::hotreload]
fn get() -> i32 {
1
}Now you can run your binary, change your library code, run cargo build, and your
application will use updated library code without restarting.
§Hotreload in release mode
By default, #[hotreload] attribute works only when debug_assertions compiler flag is
enabled, so you can keep this attribute even when building your application for production - it
will do nothing in this case. However, if you want to use hotreload in release builds as well,
pass always argument to the attribute like this: #[hotreload(always)]. This forces
hotcode to reload your library code even in release mode.
#[hotcode::hotreload(always)]
fn get() -> i32 { 1 }§How it works
hotcode adds an if block at the beginning of affected functions. It checks whether the
currently invoked function is loaded from a dynamic library or is statically linked (cdylib
and lib parts of your crate-type config, respectively). If it is located in a dynamic
module, then the body of your function is invoked; otherwise, hotcode tries to lazily load
your function from the dynamic module. The first call requires multiple initialization steps:
- perform a syscall to check whether the entered function is from a dynamic module or is statically linked, cache result;
- create a copy of your crate’s dynamic library file (so that it doesn’t block the source file for subsequent rebuilds);
- start a file watcher over the copy of your dynamic library file in a separate thread;
- load your crate as a dynamic library from its copy on disk, store it in a static cache;
- find your function’s symbol in your dynamic library, store it in a static cache;
- invoke your function loaded from the dynamic library.
All these steps can add a delay of several milliseconds. However, as soon as the library is loaded and the pointer to your function is cached, any subsequent calls will have very little overhead, because all that’s left to do is:
- read already initialized
LazyLockbool static variable to determine whether the entered function is in a dynamically or statically linked module (no syscalls performed); - find your library in
IndexMapcache by its&strname; - find your function’s pointer in
IndexMapcache by its&strname; - invoke your function loaded from the dynamic library.
This adds a flat overhead of approximately 100ns on modern CPUs.
When you rebuild your library, the system notifies hotcode’s file watcher, and it reloads your
library in the static cache from the newly copied version of your rebuilt library. Handles to
dynamic libraries are stored behind Arc and are atomically swapped during reload. Each
hot-reloadable function invocation creates Arc clone of the dynamic library handle for the
duration of the function’s execution. This ensures that a dynamic library won’t be unloaded
from memory while there are still functions using it.
§Benchmarks
The impact on performance was measured by benchmarking a Fibonacci function with and without
#[hotreload] attribute. Two implementations were benchmarked: one that uses recursion and
is relatively slow, and another that does not use recursion and is relatively fast.
Benchmark results:
| CPU \ implementation | slow, ns | hotreload slow, ns | fast, ns | hotreload fast, ns |
|---|---|---|---|---|
| i7-13700H | 18814 | 18499 | 5.4211 | 98.726 |
Slow implementation did not notice any changes, but fast implementation became ~19 times slower.
To run benchmarks locally, open benchmarks folder in the source code repository root
and run cargo build --release --lib before running cargo bench.
§Limitations
- Cannot hot-reload functions with generics, because they are generally not supported in dynamic libraries.
- Changing function ABI without changing its call sites is Undefined Behavior. For example, if
you have the following code:
and then change it to:
fn foo() { bar("warpten") } #[hotreload] fn bar(str: &str) { println!("{str:?}") }you will get UB becausefn foo() { bar(&[1, 2, 3]) } #[hotreload] fn bar(array: &[i32]) { println!("{array:?}") }foowas not hot-reloaded - it still passes string instead of array.
§Examples
Examples of desired project structure and supported functions are located in examples
folder in the source code repository root. The simplest example is the single_crate project.
Attribute Macros§
- hotreload
- The whole point. Apply it to a function inside
cdyliblibrary crate to make it hot-reloadable. Refer tohotcodecrate documentation for more information.