#hot-reload #hot-reloading #autoreload #live-reload #deserialize #live-programming

hot-lib-reloader

用于在更改时重新加载库的工具。用于加快反馈周期。

20 个版本

0.7.0 2024年1月10日
0.6.5 2023年2月4日
0.6.4 2022年10月27日
0.1.0 2022年7月31日

开发工具 类别中排名第 167

Download history 382/week @ 2024-04-22 205/week @ 2024-04-29 208/week @ 2024-05-06 192/week @ 2024-05-13 108/week @ 2024-05-20 71/week @ 2024-05-27 105/week @ 2024-06-03 89/week @ 2024-06-10 96/week @ 2024-06-17 137/week @ 2024-06-24 21/week @ 2024-07-01 44/week @ 2024-07-08 21/week @ 2024-07-15 96/week @ 2024-07-22 111/week @ 2024-07-29 49/week @ 2024-08-05

每月下载量 280
hotline-rs 中使用

MIT 许可证

73KB
635 代码行

hot-lib-reloader

Crates.io CI License

hot-lib-reloader 是一个开发工具,允许您重新加载运行中的 Rust 程序的功能。这允许进行“实时编程”,即在运行程序中修改代码并立即看到效果。

此工具基于 libloading crate,并要求您将想要热重载的代码放入 Rust 库(dylib)中。有关想法和实现的详细讨论,请参阅 这篇博客文章

有关演示和说明,请参阅 这个 Rust and Tell 演示

目录

用法

要快速生成支持热重载的新项目,您可以使用 cargo generate 模板:cargo generate rksm/rust-hot-reload

先决条件

macOS

在macOS上,可重载库需要签名。为此,hot-lib-reloader 将尝试使用 XCode 命令行工具包中的 codesign 二进制文件。建议确保这些已安装

其他平台

应该能直接使用。

示例项目设置

假设你使用了一个具有以下布局的工作区项目

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

可执行文件

使用名为 bin 的根项目设置工作区,并在 ./Cargo.toml 中定义

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

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

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

./src/main.rs 中,使用 hot_lib_reloader_macro::hot_module 属性宏定义一个子模块,该宏封装了库导出的函数

// The value of `dylib = "..."` should be the library containing the hot-reloadable functions
// It should normally be the crate name of your sub-crate.
#[hot_lib_reloader::hot_module(dylib = "lib")]
mod hot_lib {
    // Reads public no_mangle functions from lib.rs and  generates hot-reloadable
    // wrapper functions with the same signature inside this module.
    // Note that this path relative to the project root (or absolute)
    hot_functions_from_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 = hot_lib::State { counter: 0 };
    // Running in a loop so you can modify the code and see the effects
    loop {
        hot_lib::step(&mut state);
        std::thread::sleep(std::time::Duration::from_secs(1));
    }
}

库应该公开函数。它应该在 ./lib/Cargo.toml 中设置 crate 类型 dylib

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

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

你希望可重载的函数应该是公开的,并且具有 #[no_mangle] 属性。请注意,您可以定义其他没有使用 no_mangle 的函数,并且您将能够与这些函数一起使用其他函数。

pub struct State {
    pub counter: usize,
}

#[no_mangle]
pub fn step(state: &mut State) {
    state.counter += 1;
    println!("doing stuff in iteration {}", state.counter);
}

运行它

  1. 开始编译库: cargo watch -w lib -x 'build -p lib'
  2. 在另一个终端中运行可执行文件: cargo run

现在更改 lib/lib.rs 中的打印语句,并查看对运行时的影响。

此外,建议使用像 cargo runcc 这样的工具。这允许同时运行库构建和应用程序。

lib-reload 事件

LibReloadObserver

您可以使用 LibReloadObserver 提供的方法获取两种类型事件的通知

这对于在库更新前后运行代码非常有用。一个用例是将状态序列化然后再反序列化,另一个是用例是驱动应用程序。

要继续上面的例子,假设我们不想每秒运行库函数 step,而只想在库更改时重新运行它。为了做到这一点,我们首先需要获取 LibReloadObserver。为此,我们可以公开一个函数 subscribe(),该函数带有 #[lib_change_subscription] 注解(该属性告诉 hot_module 宏为其提供实现)

#[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 {}
}

然后主函数只需等待重新加载事件

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();
    }
}

如何在序列化/反序列化时阻止重新加载的示例,请参阅重新加载事件示例

was_updated 标志

为了只是确定库是否已更改,可以公开一个简单的测试函数

#[hot_lib_reloader::hot_module(dylib = "lib")]
mod hot_lib {
    /* ... */
    #[lib_updated]
    pub fn was_updated() -> bool {}
}

hot_lib::was_updated() 在库重新加载后第一次被调用时会返回 true。然后它将在再次发生重新加载之前返回 false。

用法提示

了解限制

从动态库重新加载代码存在一些需要注意的事项,这些事项在这里进行了详细讨论。

不允许更改签名

当热重载函数的签名改变时,可执行文件期望的参数和结果类型与库提供的类型不同。在这种情况下,你可能会看到崩溃。

类型更改需要小心处理

在可执行文件和库中都使用的结构体和枚举的类型不能随意更改。如果类型布局不同,你会遇到未定义的行为,这可能会导致崩溃。

有关解决此问题的方法,请参阅使用序列化

热重载函数不能是泛型的

由于 #[no_mangle] 不支持泛型,因此泛型函数不能在库中命名/查找。

可重载代码中的全局状态

如果你的热重载库包含全局状态(或依赖于隐藏全局状态的库),在重新加载后需要重新初始化它。对于隐藏全局状态从用户的库来说,这可能是个问题。如果你需要使用全局状态,尽量将其保留在可执行文件内部,并在可能的情况下将其传递给可重载的函数。

注意,“全局状态”不仅仅是全局变量。如这个问题所述,依赖于类型TypeId的包(如大多数 ECS 系统)将期望类型/ID 映射保持不变。然而,重新加载后,类型将具有不同的 ID,这使得(反)序列化更具挑战性。

使用功能标志在热重载和静态代码之间切换

请参阅reload-feature 示例以了解完整的项目。

Cargo 允许通过特性标志指定可选依赖项和条件编译。当你定义这样的特性

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

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

然后根据条件在调用可重载函数的代码中分别使用正常的或热模块,你可以无缝地在应用程序的静态和热重载版本之间切换

#[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 { /*...*/ }

要运行静态版本,只需使用 cargo run,而要运行热重载版本,请使用 cargo run --features reload

在发布模式下禁用 #[no-mangle]

要避免在发布模式下使用 #[no_mangle] 暴露函数而带来的性能损失(因为所有内容都是静态编译的,见上一条提示),且不需要导出任何函数,你可以使用no-mangle-if-debug 属性宏。它将根据你是否构建发布或调试模式有条件地禁用名称混淆。

使用序列化或泛型值更改类型

如果你想在开发时迭代状态,你可以选择将其序列化。如果你使用泛型值表示,如 serde_json::Value,你不需要字符串或二进制格式,通常甚至不需要克隆任何内容。

以下是一个示例,其中我们创建了一个包含内部 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));
    }
}

在库中,我们现在可以按需更改 InnerState 的值和类型布局

#[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 = 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(),
    }
}

还可以在库需要重新加载之前进行序列化,然后立即反序列化。这可以在reload-events 示例中看到。

使用热重载友好的应用程序结构

是否容易使用热重载取决于您如何设计应用程序。特别是,“功能核心,命令壳”模式可以轻松分割状态和行为,并且与hot-lib-reloader配合良好。

例如,对于具有主循环控制的主游戏,在主函数中设置外部状态,然后将其传递给一个fn update(state: &mut State)和一个fn render(state: &State),这是一种直接获取两个热重载函数的方法。

即使使用接管控制的框架,也可能有让它调用热重载代码的方法。例如,在bevy 示例中,系统函数可以被设置为热重载,展示了它是如何工作的。请参阅eguitokio示例中可能的设置。

调整文件监视器的防抖持续时间

hot_module宏允许设置file_watch_debounce属性,该属性定义了文件更改的防抖持续时间(以毫秒为单位)。默认为500毫秒。如果您看到在单次重新编译(可能是因为库非常大)中触发了多个更新,请增加该值。您可以尝试将其降低以实现更快的重新加载。对于小型库/快速硬件,50毫秒或20毫秒应该可以正常工作。

#[hot_module(dylib = "lib", file_watch_debounce = 50)]
/* ... */

更改动态库文件的名称和位置

默认情况下,hot-lib-reloader 假设动态库将位于 $CARGO_MANIFEST_DIR/target/debug/$CARGO_MANIFEST_DIR/target/release 文件夹中,具体取决于是否使用调试或发布配置文件。库的名称由 dylib = "..." 这部分 #[hot_module(...)] 宏定义。因此,通过指定 #[hot_module(dylib = "lib")] 并使用调试设置进行构建,hot-lib-reloader 将尝试在 MacOS 上加载 target/debug/liblib.dylib,在 Linux 上加载 target/debug/liblib.so,在 Windows 上加载 target/debug/lib.dll

如果应从不同位置加载库,可以通过设置 lib_dir 属性来指定,例如

#[hot_lib_reloader::hot_module(
    dylib = "lib",
    lib_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/target/debug")
)]
mod hot_lib {
    /* ... */
}

调整动态库的文件名

hot_module 宏允许使用 loaded_lib_name_template 参数设置影子文件名。当多个进程尝试热重载相同的库时,这很有用,可以防止冲突。此属性允许使用可以动态替换的占位符

占位符 描述 功能标志
{lib_name} 在您的代码中定义的库的名称
{load_counter} 每个热重载的增量计数器
{pid} 运行应用程序的进程 ID
{uuid} 一个 UUID v4 字符串 uuid

如果您没有指定 loaded_lib_name_template 参数,则默认命名约定用于影子文件名。此默认模式为:{lib_name}-hot-{load_counter}

#[hot_lib_reloader::hot_module(
    dylib = "lib",
    // Might result in the following shadow file lib_hot_2644_0_5e659d6e-b78c-4682-9cdd-b8a0cd3e8fc6.dll
    // Requires the 'uuid' feature flags for the {uuid} placeholder
    loaded_lib_name_template = "{lib_name}_hot_{pid}_{load_counter}_{uuid}"
)]
mod hot_lib {
    /* ... */
}

调试

如果您的 hot_module 导致编译错误,请尝试运行 cargo expand 来查看生成的代码。

默认情况下,hot-lib-reloader 包不会写入 stdout 或 stderr,但它使用 log crate 使用 info、debug 和 trace 日志级别记录其操作。根据您使用的日志框架(例如 env_logger),您可以通过设置 RUST_LOG 过滤器来启用这些日志,例如 RUST_LOG=hot_lib_reloader=trace

示例

示例可以在 rksm/hot-lib-reloader-rs/examples 找到。

  • minimal:基本设置。
  • reload-feature:使用功能在动态和静态版本之间切换。
  • serialized-state:显示允许自由修改类型和状态的功能选项。
  • reload-events:如何阻止重载以进行序列化/反序列化。
  • all-optionshot_module 宏接受的全部选项。
  • bevy:展示如何热重载 bevy 系统。
  • nannou:使用 nannou 进行交互式生成艺术。
  • egui:如何热重载原生 egui/eframe 应用。
  • iced:如何热重载 iced 应用。

已知问题

tracing crate

tracing crate 一起使用时,可能会出现多个问题

  • 当在重载的库中使用 tracing 时,应用程序有时会崩溃,出现 Attempted to register a DefaultCallsite that already exists!
  • 当与 bevy 结合使用时,在重载后,commands.insert(component) 操作将停止工作,这可能是由于内部状态混乱造成的。

如果可能的话,不要将 hot-lib-reloadertracing 结合使用。

许可

MIT

许可:MIT

依赖项

~2–10MB
~105K SLoC