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
每月下载量 280 次
在 hotline-rs 中使用
73KB
635 代码行
hot-lib-reloader
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);
}
运行它
- 开始编译库:
cargo watch -w lib -x 'build -p lib'
- 在另一个终端中运行可执行文件:
cargo run
现在更改 lib/lib.rs
中的打印语句,并查看对运行时的影响。
此外,建议使用像 cargo runcc 这样的工具。这允许同时运行库构建和应用程序。
lib-reload 事件
LibReloadObserver
您可以使用 LibReloadObserver
提供的方法获取两种类型事件的通知
wait_for_about_to_reload
被监视的库即将被重载(但旧版本仍然被加载)wait_for_reload
被监视的库已重新加载了新版本
这对于在库更新前后运行代码非常有用。一个用例是将状态序列化然后再反序列化,另一个是用例是驱动应用程序。
要继续上面的例子,假设我们不想每秒运行库函数 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 示例中,系统函数可以被设置为热重载,展示了它是如何工作的。请参阅egui和tokio示例中可能的设置。
调整文件监视器的防抖持续时间
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-options:
hot_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-reloader
与 tracing
结合使用。
许可
许可:MIT
依赖项
~2–10MB
~105K SLoC