Hot reload
A Rust library is swapped while the editor runs: save a file, rusteal watch builds and deploys it, the editor reloads it, and the objects in the level carry on with the new code. This chapter is how the pieces fit, with the files to read for each. The repository’s docs/hot-reload.md keeps the longer notes: shapes in detail, the timings, and what was and was not tested.
The loop
save a .rs file
└─ rusteal watch (polls every 400 ms, settles 300 ms)
└─ rusteal build: cargo build, then deploy
└─ copy to librusteal.so.partial, rename over librusteal.so
└─ RustealAutoReload (DirectoryWatcher, quiet for 0.5 s)
└─ RustealReloadLibrary(Name)
├─ 1. drop the Rust data of the library's objects
├─ 2. unload, load librusteal_hot_<N>.so, rusteal_init
├─ 3. reinstance the replaced classes (FReload)
├─ 4. stop here if the load failed
└─ 5. construct the Rust data again
Registering and loading a library (RustealModule.cpp)
StartupModulefills the API table, registers a delete listener and the core ticker (TickLibraries, which calls each library’son_tick), and registers the game’s library: the project’s name, the package/Script/Rusteal, the file<Project>/Plugins/Rusteal/Binaries/<Platform>/librusteal.<ext>. A Rusteal plugin’s C++ module registers its own withRustealRegisterPluginLibrary(<Plugin>/Binaries/<Platform>/librusteal_<Plugin>.<ext>, package/Script/<Plugin>).RegisterLibrarydeletes the stale load copies (<stem>_hot_*.<ext>) a crash may have left, and broadcastsRustealOnLibraryRegistered, which the editor module listens to.LoadLibraryincrements the library’s reload count and makes the load copy<stem>_hot_<N>.<ext>: a hard link on Linux and macOS, a copy where that fails or on Windows (a loaded DLL’s file stays locked), the deployed file itself if both fail. It checks the copy’srusteal_versionagainst the plugin’s descriptorVersion, callsrusteal_init(&Table)with that library current (FRustealLibraryScope) and keeps the callbacks it returns.UnloadLibrarycalls the library’son_shutdownandrusteal_shutdown, forgets its pinned objects and console commands, frees the handle, and deletes its load copy.
Loading a copy is what lets the build replace the deployed file at any time. It is also why the deploy step must replace the file (.partial then rename, a new inode) and never write into it: the load copy may be a hard link to the old inode, which the running editor has mapped.
Reloading (ReloadLibrary)
Each phase is timed and logged:
drop_rust_instanceon every object of aURustealReifiedClassowned by the library. The#[uproperty]fields live in the objects’ memory and stay; the Rust-private fields are dropped and come back fromDefault.- Unload, then load the new copy, whose
rusteal_initregisters every class again. RustealReifyReinstanceReplacedClasses().- If the load failed, stop and log why.
construct_rust_instanceon every object, saying whether it is a class default object.
Rusteal.Reload [Names...] (an FAutoConsoleCommand of the runtime module) reloads the named libraries, or all of them.
Kept or replaced: the shape
#[uclass] and #[uclass_impl] compute a shape at compile time (rusteal-macros/src/shape.rs): an FNV-1a hash over the sorted declarations, the class’s arguments, its #[uproperty] and #[component] fields and its #[ufunction] and #[udelegate] signatures. Method bodies, doc comments, private fields and the order of declarations are left out.
CreateClassImpl (RustealReifyApiImpl.cpp) finds the existing class by name:
- same shape and parent: the class is kept, with its new
RustTypeIdandLibrary.AddFunctionImplkeeps each existingURustealReifiedFunctionand only gives it the newCallbackIdandLibrary, so a call goes to the new code. This is the common case, a change to a function’s body, and it costs nothing. - changed, with a reinstancer:
RetireClasscuts the old class and its functions from the library (Library = nullptr, so they do nothing), renames it and its stub Blueprint toRUSTEAL_<Name>in the transient package, and roots it; a new class is created and the pair is queued for reinstancing. - changed, without a reinstancer (a
-gameprocess): the class is kept as it was, with a warning that only the editor can apply the change.
Structs and enums are not hot reloaded: create_struct and create_enum return the existing object and warn when its fields or values changed, which takes an editor restart.
Reinstancing (RustealEditorModule.cpp)
The editor module registers ReinstanceClasses with RustealSetClassReinstancer:
FReload Reload(EActiveReloadType::Reinstancing, "RUSTEAL", *GLog);NotifyChange(New, Old)for each replaced class, thenReinstance(): the engine moves every object, Blueprint child and default object to the new class, as it does for Live Coding;CompileBlueprintsUsing: the engine recompiles the replaced class’s children, and Rusteal also recompiles the loaded Blueprints that only use it (a Level Blueprint calling one of its functions), which would otherwise call a function that no longer exists;Finalize(false), with garbage collection skipped, so a reload does not stall the editor.
FReload lives in UnrealEd, which is why reinstancing is the editor module’s job and a -game process has none.
Reloading on deploy (RustealAutoReload.cpp)
The editor module watches each library’s deploy directory with the engine’s DirectoryWatcher, for the libraries already registered (RustealForEachLibrary) and the ones registered later (RustealOnLibraryRegistered). Only a change to the deployed file itself counts: .partial and _hot_ files are ignored. Once the file has been quiet for half a second (SettleSeconds), the ticker reloads that library and shows an editor notification, for 3 seconds on success and 6 on failure. Rusteal.AutoReload 0 turns it off.
Limits
| Change | Result |
|---|---|
| A function’s body, a private field’s use | The class is kept; the functions point to the new code. |
A #[uproperty], #[component], #[ufunction] or #[udelegate] added, removed or changed | The class is replaced and reinstanced, its Blueprint children and users recompiled. |
A #[ustruct] or #[uenum] changed | Kept as it was, with a warning: restart the editor. |
| A function a Blueprint uses removed | The Blueprint’s node breaks, as it would with C++. |
| Private Rust fields | Reset to Default on every reload. |
The C++ plugin, the bindings, rusteal.toml | Not reloadable: rusteal build --all and an editor restart. |
Why not Live Coding
Live Coding is Windows-only and patches the machine code UBT compiled; a library Cargo built has nothing for it to patch. What Rusteal needs from it is the reinstancing, and that part, FReload, is exported from UnrealEd (UNREALED_API) and usable on every platform. The function table makes the code swap itself trivial: since every reified function calls through its library and callback id, loading the new library and pointing the functions at it is the whole swap.