Rust 热重载革新:即时编程的终极指南与实践:hot-lib-reloader
博主找工作贴:主做大前端,9 年工作经验,正在找工作,base 南京,上海,苏州都可,希望有招聘方可以联系或内推。
hot-lib-reloader
是一个开发工具,允许你在 Rust 程序运行时重新加载函数。这使得你可以进行“即时编程”,在修改代码后立即在运行的程序中看到效果。
这个工具围绕 libloading crate[1] 构建,需要你将想要热重载的代码放入一个 Rust 库(dylib)。关于这个想法和实现的详细讨论,请参阅 这篇博客文章[2]。
对于演示和解释,也请参见 这个 Rust and Tell 演讲[3]。
使用说明
要快速生成一个支持热重载的新项目,你可以使用 cargo generate[4] 模板:cargo generate rksm/rust-hot-reload
。
先决条件
macOS
在 macOS 上,可重载的库需要进行代码签名。为此,hot-lib-reloader 将尝试使用 XCode 命令行工具中包含的 codesign
二进制文件。建议确保 它们已安装[5]。
其他平台
应该可以直接使用。
示例项目设置
假设你使用一个工作空间项目,具有以下布局:
├── Cargo.toml
└── src
│ └── main.rs
└── lib
├── Cargo.toml
└── src
└── lib.rs
可执行文件
使用 ./Cargo.toml
中名为 bin
的根项目设置工作空间:
[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
] 属性宏包装由库导出的函数:
// `dylib = "..."` 的值应该是包含热重载函数的库
// 它通常应该是你的子 crate 的 crate 名称。
#[hot_lib_reloader::hot_module(dylib = "lib")]
mod hot_lib {
// 从 lib.rs 中读取公共 no_mangle 函数,并在此模块内生成具有相同签名的热重载
// 包装函数。注意这个路径是相对于项目根目录的(或绝对路径)
hot_functions_from_file!("lib/src/lib.rs");
// 因为我们生成的函数具有完全相同的签名,
// 我们需要导入使用的类型
pub use lib::State;
}
fn main() {
let mut state = hot_lib::State { counter: 0 };
// 循环运行,这样你就可以修改代码并看到效果
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!("在迭代 {} 中执行操作", state.counter);
}
运行它
启动库的编译: cargo watch -w lib -x 'build -p lib'
在另一个终端运行可执行文件: cargo run
现在,例如改变 lib/lib.rs
中的打印语句,看看运行时的效果。
此外,推荐使用像 cargo runcc[6] 这样的工具。这允许你一次性运行库构建和应用程序。
库重载事件
LibReloadObserver
你可以使用 [LibReloadObserver
] 提供的方法来获取两种事件的通知:
`wait_for_about_to_reload`[7] 观察到的库即将被重载(但旧版本仍然加载) `wait_for_reload`[8] 观察到的库的新版本刚刚被重载
这对于在库更新前后运行代码非常有用。一个用例是序列化然后反序列化状态,另一个是驱动应用程序。
继续上面的例子,假设我们不想每秒运行一次库函数 step
,我们只想在库更改时重新运行它。为了做到这一点,我们首先需要获得 LibReloadObserver
。为此,我们可以公开一个带有 #[lib_change_subscription]
注释的 subscribe()
函数(该属性告诉 hot_module
宏提供它的实现):
#[hot_lib_reloader::hot_module(dylib = "lib")]
mod hot_lib {
/* 上面的代码 */
// 公开一个类型来订阅库加载事件
#[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);
// 阻塞直到库被重载
lib_observer.wait_for_reload();
}
}
如何在阻止重载以进行序列化/反序列化方面,请参阅 reload-events 示例[9]。
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。
使用技巧
了解限制
从动态库重新加载代码带来了一些需要注意的事项,这些在 这里[10] 有详细讨论。
不要更改签名
当热重载函数的签名更改时,可执行文件期望的参数和结果类型与库提供的不一致。在这种情况下,你很可能会看到崩溃。
类型更改需要小心
在可执行文件和库中都使用的 struct 和 enum 的类型不能随意更改。如果类型的布局不同,你会遇到未定义的行为,这很可能会导致崩溃。
见 使用序列化[11] 了解解决方法。
热重载函数不能是泛型的
由于 #[no_mangle]
不支持泛型,泛型函数不能在库中被命名 / 找到。
可重载代码中的全局状态
如果你的热重载库包含全局状态(或依赖于隐藏全局状态的库),你需要在重载后重新初始化它。这可能是一个问题,因为库可能会从用户那里隐藏全局状态。如果需要使用全局状态,请尽可能将其保留在可执行文件中,并在需要时将其传递给可重载函数。
还要注意,“全局状态”不仅仅是全局变量。正如 这个问题[12] 中指出的,依赖于类型 TypeId[13] 的 crates(像大多数 ECS 系统那样)会期望类型/id 映射是恒定的。重载后,类型将具有不同的 ids,这使得(反)序列化更具挑战性。
使用特性标志在热重载和静态代码之间切换
请参阅 reload-feature 示例[14] 以获取完整的项目。
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 属性宏[15]。它将根据你是在构建发布还是调试模式来条件性地禁用名称混淆。
使用序列化或通用值来更改类型
如果你想在开发过程中迭代状态,你可以选择序列化它。如果你使用像 serde_json::Value[16] 这样的通用值表示,你就不需要字符串或二进制格式,通常甚至不需要克隆任何东西。
这里有一个示例,我们创建一个状态容器,它有一个内部的 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 {});
// 你可以在这里自由地修改 InnerState 布局和 state.inner 值!
State {
inner: serde_json::to_value(inner).unwrap(),
}
}
或者,你也可以在库即将被重载之前进行序列化,然后在重载后立即反序列化。这在 reload-events 示例[17] 中有展示。
使用热重载友好的应用程序结构
热重载是否易于使用取决于你如何构建你的应用程序。特别是,["functional core, imperative shell" pattern](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell ""functional core, imperative shell" pattern") 使得它很容易将状态和行为分开,并且与 hot-lib-reloader
很好地配合。
例如,对于一个简单的游戏,你可以在主函数中设置外部状态,然后将其传递给 fn update(state: &mut State)
和 fn render(state: &State)
,这是获得两个热重载函数的直接方法。
但即使你使用的框架控制了流程,很可能也有办法让它调用热重载的代码。bevy 示例[18] 展示了如何使系统函数热重载,展示了这是如何工作的。查看 egui[19] 和 tokio[20] 示例可能的设置。
调整文件监视去抖动持续时间
hot_module
宏允许设置 file_watch_debounce
属性,它定义了文件更改的去抖动持续时间(毫秒)。默认情况下这是 500ms。如果你看到一次重新编译触发了多次更新(可能会发生库非常大),增加这个值。你可以尝试减少它以获得更快的重载。对于小型库 / 快速硬件,50ms 或 20ms 应该可以正常工作。
#[hot_module(dylib = "lib", file_watch_debounce = 50)]
/* ... */
更改 dylib 文件的名称和位置
默认情况下,hot-lib-reloader
假设将在 $CARGO_MANIFEST_DIR/target/debug/
或 $CARGO_MANIFEST_DIR/target/release
文件夹中有一个动态库可用,具体取决于是使用调试还是发布配置文件。库的名称由 #[hot_module(...)]
宏的 dylib = "..."
部分定义。因此,通过指定 #[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 {
/* ... */
}
调整 dylib 文件名
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",
// 可能会导致这样的阴影文件 lib_hot_2644_0_5e659d6e-b78c-4682-9cdd-b8a0cd3e8fc6.dll
// 需要 `{uuid}` 占位符的 'uuid' 特性标志
loaded_lib_name_template = "{lib_name}
_hot_{pid}_{load_counter}_{uuid}"
)]
mod hot_lib {
/* ... */
}
调试
如果你的 hot_module
给出了奇怪的编译错误,请尝试使用 cargo expand
查看生成的代码。
默认情况下,hot-lib-reloader
crate 不会写入 stdout 或 stderr,而是使用 log crate[21] 在 info、debug 和 trace 日志级别记录它的操作。根据你使用的日志框架(例如 env_logger[22]),你可以通过设置 RUST_LOG
过滤器(如 RUST_LOG=hot_lib_reloader=trace
)来启用这些日志。
示例
示例可以在 rksm/hot-lib-reloader-rs/examples[23] 找到。
minimal[24]: 基础设置。 reload-feature[25]: 使用特性在动态和静态版本之间切换。 serialized-state[26]: 展示了一个允许自由修改类型和状态的选项。 reload-events[27]: 如何阻止重载以进行序列化 / 反序列化。 all-options[28]: hot_module
宏接受的所有选项。bevy[29]: 如何使 bevy 系统热重载。 nannou[30]: 使用 nannou[31] 的交互式生成艺术。 egui[32]: 如何热重载一个原生 egui / eframe 应用程序。 iced[33]: 如何热重载一个 iced 应用程序。
已知问题
tracing crate
当与 tracing
crate 结合使用时,可能会发生多个问题:
当在被重载的库中使用 tracing
时,应用程序有时会因Attempted to register a DefaultCallsite that already exists!
而崩溃。与 bevy 结合使用时,在重载后 commands.insert(component)
操作可能不再工作,很可能是因为内部状态被搞乱了。
如果可以的话,不要将 hot-lib-reloader
与 tracing
结合使用。
附录
https://github.com/rksm/hot-lib-reloader-rs
libloading crate: https://crates.io/crates/libloading
[2]这篇博客文章: https://robert.kra.hn/posts/hot-reloading-rust
[3]这个 Rust and Tell 演讲: https://www.youtube.com/watch?v=-UUImyqX8j0
[4]cargo generate: https://cargo-generate.github.io/cargo-generate/
[5]它们已安装: https://mac.install.guide/commandlinetools/
[6]cargo runcc: https://crates.io/crates/runcc
[7]wait_for_about_to_reload
: LibReloadObserver::wait_for_about_to_reload
wait_for_reload
: LibReloadObserver::wait_for_reload
reload-events 示例: https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/reload-events
[10]这里: https://robert.kra.hn/posts/hot-reloading-rust/#caveats-and-asterisks
[11]使用序列化: #使用序列化或通用值来更改类型
[12]这个问题: https://github.com/rksm/hot-lib-reloader-rs/issues/34
[13]TypeId: https://doc.rust-lang.org/std/any/struct.TypeId.html
[14]reload-feature 示例: https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/reload-feature
[15]no_mangle]` 暴露函数时的性能损失(见上一个提示),在那里一切都是静态编译的(不需要导出任何函数),你可以使用 [no-mangle-if-debug 属性宏: ./macro-no-mangle-if-debug
[16]serde_json::Value: https://docs.rs/serde_json/latest/serde_json/value/enum.Value.html
[17]reload-events 示例: https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/reload-events
[18]bevy 示例: https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/bevy
[19]egui: https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/hot-egui
[20]tokio: https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/reload-events
[21]log crate: https://crates.io/crates/log
[22]env_logger: https://crates.io/crates/env_logger
[23]rksm/hot-lib-reloader-rs/examples: https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples
[24]minimal: https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/minimal
[25]reload-feature: https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/reload-feature
[26]serialized-state: https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/serialized-state
[27]reload-events: https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/reload-events
[28]all-options: https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/all-options
[29]bevy: https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/bevy
[30]nannou: https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/nannou-vector-field
[31]nannou: https://nannou.cc
[32]egui: https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/hot-egui
[33]iced: https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/hot-iced