notes/architecture/07-HOTRELOAD.md
Dioxus supports two complementary hot-reload systems: RSX template hot-reload for UI changes, and Subsecond hot-patching for Rust code changes.
Subsecond implements hot-patching through jump table indirection:
subsecond::call() or HotFn::current()APP_JUMP_TABLEAdvantage: Decouples running binary from patched code. Safe memory model.
1. ThinLink compiles only modified functions → patch dylib
2. Patch sent via devtools WebSocket
3. subsecond::apply_patch() loads via libloading::Library
4. Base address calculated using main as anchor
5. Jump table updated with old→new address mappings
Operating systems randomize memory addresses (ASLR), so compiled addresses don't match runtime.
Solution:
main via dlsym()/GetProcAddress()old_offset = aslr_reference - table.aslr_reference(old_address + old_offset) → (new_address + new_offset)TLS presents challenges for hot-patching:
Why: New thread-local variables exist in patch library's TLS segment, separate from main executable's TLS. Subsecond doesn't rebind TLS accesses.
RSX hot reload is orthogonal to subsecond. While subsecond reloads Rust functions, RSX hot reload handles template literal values:
Hot-reloadable:
"{variable}"Component { value: 123 }"{expression}"Not hot-reloadable:
Conservative approach: If Rust code changes, not hot-reloadable.
1. Parse old and new files
2. Extract all rsx! macro invocations
3. Replace all rsx! bodies with empty rsx! {}
4. Remove doc comments
5. Compare modified files
6. If identical → Rust unchanged → proceed with template diff
7. If different → requires full rebuild
Three pools of reusable items from last build:
"{class}", "{id}"Each item tracks usage with Cell<bool> for waste scoring.
For each new dynamic node:
NOT possible:
rsx! macros changesIS possible:
Bidirectional WebSocket between app and devserver.
Connection:
ws://localhost:3000/_dioxusbuild_id, pid, aslr_referencepub enum DevserverMsg {
HotReload(HotReloadMsg), // Templates + optional jump table
HotPatchStart, // Binary patching starting
FullReloadStart, // Rebuilding entire app
FullReloadFailed, // Build failed
FullReloadCommand, // Full page reload needed
Shutdown, // Devserver shutting down
}
pub struct HotReloadMsg {
pub templates: Vec<HotReloadTemplateWithLocation>,
pub assets: Vec<PathBuf>,
pub ms_elapsed: u64,
pub jump_table: Option<JumpTable>,
pub for_build_id: Option<u64>,
pub for_pid: Option<u32>,
}
connect(callback) - Generic connectionconnect_subsecond() - Subsecond-specific with jump tablesapply_changes(dom, msg):
ws://host/_dioxus?build_id={build_id}FullReloadCommanddx_force_reload query paramsfn main() {
dioxus::launch(app);
// Devtools automatically connects during init
}
For non-Dioxus apps:
fn main() {
dioxus_devtools::connect_subsecond();
loop {
dioxus_devtools::subsecond::call(|| {
handle_request()
});
}
}
#[tokio::main]
async fn main() {
dioxus_devtools::serve_subsecond_with_args(
state,
|state| async {
app_main(state).await
}
).await;
}
Process:
Fat Build (initial):
HotpatchModuleCacheThin Build (patches):
pub struct JumpTable {
pub lib: PathBuf, // Path to patch dylib
pub map: HashMap<u64, u64>, // old_addr → new_addr
pub aslr_reference: u64,
pub new_base_address: u64,
pub ifunc_count: u32,
}
create_windows_jump_table() - x86/x64 jump stubscreate_native_jump_table() - macOS/Linux function overridescreate_wasm_jump_table() - WASM indirect call table updates