From 10bbe44c3024fbd1b0b7b274234788632c1bfa3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 6 Jun 2025 22:58:59 +0200 Subject: [PATCH 1/4] Draft experimental hotpatching support :tada: Thanks to `subsecond` by the Dioxus folks! --- Cargo.lock | 85 +++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + debug/Cargo.toml | 5 ++- debug/src/lib.rs | 32 ++++++++++++++- runtime/src/lib.rs | 5 +++ src/application.rs | 22 +++++++---- src/application/timed.rs | 25 ++++++++---- src/daemon.rs | 24 ++++++++---- winit/src/lib.rs | 40 +++++++++++++++++-- winit/src/proxy.rs | 8 ++-- 10 files changed, 212 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b90d4cec..10abae26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -519,6 +519,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -702,6 +722,17 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "cargo-hot-protocol" +version = "0.1.0" +source = "git+https://github.com/hecrj/cargo-hot.git?rev=e71ddcd1d37be79ddb6f22262600798319235ba5#e71ddcd1d37be79ddb6f22262600798319235ba5" +dependencies = [ + "anyhow", + "bincode 2.0.1", + "log", + "subsecond", +] + [[package]] name = "cast" version = "0.3.0" @@ -2419,7 +2450,7 @@ dependencies = [ name = "iced_beacon" version = "0.14.0-dev" dependencies = [ - "bincode", + "bincode 1.3.3", "futures", "iced_core", "log", @@ -2451,6 +2482,7 @@ dependencies = [ name = "iced_debug" version = "0.14.0-dev" dependencies = [ + "cargo-hot-protocol", "iced_beacon", "iced_core", "iced_futures", @@ -3244,6 +3276,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memfd" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" +dependencies = [ + "rustix 0.38.44", +] + [[package]] name = "memmap2" version = "0.9.5" @@ -5406,6 +5447,34 @@ dependencies = [ "rayon", ] +[[package]] +name = "subsecond" +version = "0.7.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5b40acd555d02d9a0b5bf4080dbf2cd085d5e2eb2ae7851cb14b9bf5af15c" +dependencies = [ + "js-sys", + "libc", + "libloading", + "memfd", + "memmap2", + "serde", + "subsecond-types", + "thiserror 2.0.12", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "subsecond-types" +version = "0.7.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bedadae58a56e137ac970c38c44bff38cee24400fef64c37d5a188a065b1ec1f" +dependencies = [ + "serde", +] + [[package]] name = "subtle" version = "2.6.1" @@ -5483,7 +5552,7 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" dependencies = [ - "bincode", + "bincode 1.3.3", "bitflags 1.3.2", "flate2", "fnv", @@ -6169,6 +6238,12 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.4" @@ -6288,6 +6363,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + [[package]] name = "visible_bounds" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 3c19290b..daa5c8e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -167,6 +167,7 @@ bincode = "1.3" bitflags = "2.0" bytemuck = { version = "1.0", features = ["derive"] } bytes = "1.6" +cargo-hot = { package = "cargo-hot-protocol", git = "https://github.com/hecrj/cargo-hot.git", rev = "e71ddcd1d37be79ddb6f22262600798319235ba5" } cosmic-text = "0.14" dark-light = "2.0" futures = { version = "0.3", default-features = false } diff --git a/debug/Cargo.toml b/debug/Cargo.toml index f6c7c843..a9ffcf38 100644 --- a/debug/Cargo.toml +++ b/debug/Cargo.toml @@ -11,7 +11,7 @@ categories.workspace = true keywords.workspace = true [features] -enable = ["dep:iced_beacon"] +enable = ["dep:iced_beacon", "dep:cargo-hot"] [dependencies] iced_core.workspace = true @@ -21,3 +21,6 @@ log.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] iced_beacon.workspace = true iced_beacon.optional = true + +cargo-hot.workspace = true +cargo-hot.optional = true diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 17463f9a..7d64cb60 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -113,6 +113,14 @@ pub fn commands() -> Subscription { internal::commands() } +pub fn hot(f: impl FnOnce() -> O) -> O { + internal::hot(f) +} + +pub fn on_hotpatch(f: impl Fn() + Send + Sync + 'static) { + internal::on_hotpatch(f) +} + #[cfg(all(feature = "enable", not(target_arch = "wasm32")))] mod internal { use crate::core::theme; @@ -129,7 +137,7 @@ mod internal { use beacon::span::present; use std::sync::atomic::{self, AtomicBool, AtomicUsize}; - use std::sync::{LazyLock, RwLock}; + use std::sync::{Arc, LazyLock, RwLock}; pub fn init(metadata: Metadata) { let name = metadata.name.split("::").next().unwrap_or(metadata.name); @@ -140,6 +148,8 @@ mod internal { theme: metadata.theme, can_time_travel: metadata.can_time_travel, }; + + cargo_hot::connect(); } pub fn quit() -> bool { @@ -271,6 +281,20 @@ mod internal { Subscription::run(listen_for_commands) } + pub fn hot(f: impl FnOnce() -> O) -> O { + let mut f = Some(f); + + // The `move` here is important. Hotpatching will not work + // otherwise. + cargo_hot::subsecond::call(move || { + f.take().expect("Hot function is stale")() + }) + } + + pub fn on_hotpatch(f: impl Fn() + Send + Sync + 'static) { + cargo_hot::subsecond::register_handler(Arc::new(f)); + } + fn span(span: span::Stage) -> Span { log(client::Event::SpanStarted(span.clone())); @@ -398,4 +422,10 @@ mod internal { impl Span { pub fn finish(self) {} } + + pub fn hot(f: impl FnOnce() -> O) -> O { + f() + } + + pub fn on_hotpatch(_f: impl Fn() + Send + Sync + 'static) {} } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 47bd92d9..457f723c 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -56,6 +56,9 @@ pub enum Action { /// Run a system action. System(system::Action), + /// Recreate all user interfaces and redraw all windows. + Reload, + /// Exits the runtime. /// /// This will normally close any application windows and @@ -79,6 +82,7 @@ impl Action { Action::Clipboard(action) => Err(Action::Clipboard(action)), Action::Window(action) => Err(Action::Window(action)), Action::System(action) => Err(Action::System(action)), + Action::Reload => Err(Action::Reload), Action::Exit => Err(Action::Exit), } } @@ -102,6 +106,7 @@ where } Action::Window(_) => write!(f, "Action::Window"), Action::System(action) => write!(f, "Action::System({action:?})"), + Action::Reload => write!(f, "Action::Reload"), Action::Exit => write!(f, "Action::Exit"), } } diff --git a/src/application.rs b/src/application.rs index e87d89a2..e735218b 100644 --- a/src/application.rs +++ b/src/application.rs @@ -38,6 +38,8 @@ use crate::{ Element, Executor, Font, Result, Settings, Size, Subscription, Task, }; +use iced_debug as debug; + use std::borrow::Cow; pub mod timed; @@ -126,7 +128,7 @@ where state: &mut Self::State, message: Self::Message, ) -> Task { - self.update.update(state, message) + debug::hot(|| self.update.update(state, message)) } fn view<'a>( @@ -134,7 +136,7 @@ where state: &'a Self::State, _window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.view.view(state) + debug::hot(|| self.view.view(state)) } } @@ -327,7 +329,7 @@ impl Application

{ > { Application { raw: program::with_title(self.raw, move |state, _window| { - title.title(state) + debug::hot(|| title.title(state)) }), settings: self.settings, window: self.window, @@ -342,7 +344,9 @@ impl Application

{ impl Program, > { Application { - raw: program::with_subscription(self.raw, f), + raw: program::with_subscription(self.raw, move |state| { + debug::hot(|| f(state)) + }), settings: self.settings, window: self.window, } @@ -356,7 +360,9 @@ impl Application

{ impl Program, > { Application { - raw: program::with_theme(self.raw, move |state, _window| f(state)), + raw: program::with_theme(self.raw, move |state, _window| { + debug::hot(|| f(state)) + }), settings: self.settings, window: self.window, } @@ -370,7 +376,9 @@ impl Application

{ impl Program, > { Application { - raw: program::with_style(self.raw, f), + raw: program::with_style(self.raw, move |state, theme| { + debug::hot(|| f(state, theme)) + }), settings: self.settings, window: self.window, } @@ -385,7 +393,7 @@ impl Application

{ > { Application { raw: program::with_scale_factor(self.raw, move |state, _window| { - f(state) + debug::hot(|| f(state)) }), settings: self.settings, window: self.window, diff --git a/src/application/timed.rs b/src/application/timed.rs index 606273c8..3d158874 100644 --- a/src/application/timed.rs +++ b/src/application/timed.rs @@ -6,6 +6,8 @@ use crate::time::Instant; use crate::window; use crate::{Element, Program, Settings, Subscription, Task}; +use iced_debug as debug; + /// Creates an [`Application`] with an `update` function that also /// takes the [`Instant`] of each `Message`. /// @@ -97,10 +99,12 @@ where state: &mut Self::State, (message, now): Self::Message, ) -> Task { - self.update - .update(state, message, now) - .into() - .map(|message| (message, Instant::now())) + debug::hot(move || { + self.update + .update(state, message, now) + .into() + .map(|message| (message, Instant::now())) + }) } fn view<'a>( @@ -108,16 +112,21 @@ where state: &'a Self::State, _window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.view - .view(state) - .map(|message| (message, Instant::now())) + debug::hot(|| { + self.view + .view(state) + .map(|message| (message, Instant::now())) + }) } fn subscription( &self, state: &Self::State, ) -> self::Subscription { - (self.subscription)(state).map(|message| (message, Instant::now())) + debug::hot(|| { + (self.subscription)(state) + .map(|message| (message, Instant::now())) + }) } } diff --git a/src/daemon.rs b/src/daemon.rs index 1b99da30..2074cf71 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -6,6 +6,8 @@ use crate::theme; use crate::window; use crate::{Element, Executor, Font, Result, Settings, Subscription, Task}; +use iced_debug as debug; + use std::borrow::Cow; /// Creates an iced [`Daemon`] given its boot, update, and view logic. @@ -72,7 +74,7 @@ where state: &mut Self::State, message: Self::Message, ) -> Task { - self.update.update(state, message) + debug::hot(|| self.update.update(state, message)) } fn view<'a>( @@ -80,7 +82,7 @@ where state: &'a Self::State, window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.view.view(state, window) + debug::hot(|| self.view.view(state, window)) } } @@ -176,7 +178,7 @@ impl Daemon

{ > { Daemon { raw: program::with_title(self.raw, move |state, window| { - title.title(state, window) + debug::hot(|| title.title(state, window)) }), settings: self.settings, } @@ -190,7 +192,9 @@ impl Daemon

{ impl Program, > { Daemon { - raw: program::with_subscription(self.raw, f), + raw: program::with_subscription(self.raw, move |state| { + debug::hot(|| f(state)) + }), settings: self.settings, } } @@ -203,7 +207,9 @@ impl Daemon

{ impl Program, > { Daemon { - raw: program::with_theme(self.raw, f), + raw: program::with_theme(self.raw, move |state, window| { + debug::hot(|| f(state, window)) + }), settings: self.settings, } } @@ -216,7 +222,9 @@ impl Daemon

{ impl Program, > { Daemon { - raw: program::with_style(self.raw, f), + raw: program::with_style(self.raw, move |state, theme| { + debug::hot(|| f(state, theme)) + }), settings: self.settings, } } @@ -229,7 +237,9 @@ impl Daemon

{ impl Program, > { Daemon { - raw: program::with_scale_factor(self.raw, f), + raw: program::with_scale_factor(self.raw, move |state, window| { + debug::hot(|| f(state, window)) + }), settings: self.settings, } } diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 7b94a878..5f1dfb22 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -85,6 +85,15 @@ where let (proxy, worker) = Proxy::new(event_loop.create_proxy()); + #[cfg(feature = "debug")] + { + let proxy = proxy.clone(); + + debug::on_hotpatch(move || { + proxy.send_action(Action::Reload); + }); + } + let mut runtime = { let executor = P::Executor::new().map_err(Error::ExecutorCreationFailed)?; @@ -527,7 +536,7 @@ async fn run_instance

( let create_compositor = { let window = window.clone(); - let mut proxy = proxy.clone(); + let proxy = proxy.clone(); let default_fonts = default_fonts.clone(); async move { @@ -1075,9 +1084,9 @@ fn update( runtime.track(recipes); } -fn run_action( +fn run_action<'a, P, C>( action: Action, - program: &program::Instance

, + program: &'a program::Instance

, compositor: &mut Option, events: &mut Vec<(window::Id, core::Event)>, messages: &mut Vec, @@ -1085,7 +1094,7 @@ fn run_action( control_sender: &mut mpsc::UnboundedSender, interfaces: &mut FxHashMap< window::Id, - UserInterface<'_, P::Message, P::Theme, P::Renderer>, + UserInterface<'a, P::Message, P::Theme, P::Renderer>, >, window_manager: &mut WindowManager, ui_caches: &mut FxHashMap, @@ -1437,6 +1446,29 @@ fn run_action( let _ = channel.send(Ok(())); } } + Action::Reload => { + for (id, window) in window_manager.iter_mut() { + let Some(ui) = interfaces.remove(&id) else { + continue; + }; + + let cache = ui.into_cache(); + let size = window.size(); + + let _ = interfaces.insert( + id, + build_user_interface( + program, + cache, + &mut window.renderer, + size, + id, + ), + ); + + window.raw.request_redraw(); + } + } Action::Exit => { control_sender .start_send(Control::Exit) diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index d8d3f4a2..dcfcacff 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -77,7 +77,7 @@ impl Proxy { /// /// Note: This skips the backpressure mechanism with an unbounded /// channel. Use sparingly! - pub fn send(&mut self, value: T) + pub fn send(&self, value: T) where T: std::fmt::Debug, { @@ -88,13 +88,11 @@ impl Proxy { /// /// Note: This skips the backpressure mechanism with an unbounded /// channel. Use sparingly! - pub fn send_action(&mut self, action: Action) + pub fn send_action(&self, action: Action) where T: std::fmt::Debug, { - self.raw - .send_event(action) - .expect("Send message to event loop"); + let _ = self.raw.send_event(action); } /// Frees an amount of slots for additional messages to be queued in From c1c5fbda613fd58a1da149321aff34a6b482da71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 7 Jun 2025 04:50:52 +0200 Subject: [PATCH 2/4] Update `cargo-hot` --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10abae26..3f517be1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -725,7 +725,7 @@ dependencies = [ [[package]] name = "cargo-hot-protocol" version = "0.1.0" -source = "git+https://github.com/hecrj/cargo-hot.git?rev=e71ddcd1d37be79ddb6f22262600798319235ba5#e71ddcd1d37be79ddb6f22262600798319235ba5" +source = "git+https://github.com/hecrj/cargo-hot.git?rev=b8dc518b8640928178a501257e353b73bc06cf47#b8dc518b8640928178a501257e353b73bc06cf47" dependencies = [ "anyhow", "bincode 2.0.1", diff --git a/Cargo.toml b/Cargo.toml index daa5c8e0..bd7caad1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -167,7 +167,7 @@ bincode = "1.3" bitflags = "2.0" bytemuck = { version = "1.0", features = ["derive"] } bytes = "1.6" -cargo-hot = { package = "cargo-hot-protocol", git = "https://github.com/hecrj/cargo-hot.git", rev = "e71ddcd1d37be79ddb6f22262600798319235ba5" } +cargo-hot = { package = "cargo-hot-protocol", git = "https://github.com/hecrj/cargo-hot.git", rev = "b8dc518b8640928178a501257e353b73bc06cf47" } cosmic-text = "0.14" dark-light = "2.0" futures = { version = "0.3", default-features = false } From 699b85762ba3cf3757c23ed9aa698d8ba3711e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 10 Jun 2025 02:32:22 +0200 Subject: [PATCH 3/4] Try to detect stale type changes when hotpatching --- debug/src/lib.rs | 55 ++++++++++++++++++++++++++++++++++++++++++--- devtools/src/lib.rs | 31 ++++++++++++++++--------- 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 7d64cb60..99def674 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -121,6 +121,10 @@ pub fn on_hotpatch(f: impl Fn() + Send + Sync + 'static) { internal::on_hotpatch(f) } +pub fn is_stale() -> bool { + internal::is_stale() +} + #[cfg(all(feature = "enable", not(target_arch = "wasm32")))] mod internal { use crate::core::theme; @@ -136,8 +140,16 @@ mod internal { use beacon::span; use beacon::span::present; + use std::collections::BTreeSet; use std::sync::atomic::{self, AtomicBool, AtomicUsize}; - use std::sync::{Arc, LazyLock, RwLock}; + use std::sync::{Arc, LazyLock, Mutex, OnceLock, RwLock}; + + static IS_STALE: AtomicBool = AtomicBool::new(false); + + static HOT_FUNCTIONS_PENDING: Mutex> = + Mutex::new(BTreeSet::new()); + + static HOT_FUNCTIONS: OnceLock> = OnceLock::new(); pub fn init(metadata: Metadata) { let name = metadata.name.split("::").next().unwrap_or(metadata.name); @@ -150,6 +162,20 @@ mod internal { }; cargo_hot::connect(); + + cargo_hot::subsecond::register_handler(Arc::new(|| { + if HOT_FUNCTIONS.get().is_none() { + HOT_FUNCTIONS + .set(std::mem::take( + &mut HOT_FUNCTIONS_PENDING + .lock() + .expect("Lock hot functions"), + )) + .expect("Set hot functions"); + } + + IS_STALE.store(false, atomic::Ordering::Relaxed); + })); } pub fn quit() -> bool { @@ -286,15 +312,34 @@ mod internal { // The `move` here is important. Hotpatching will not work // otherwise. - cargo_hot::subsecond::call(move || { + let mut f = cargo_hot::subsecond::HotFn::current(move || { f.take().expect("Hot function is stale")() - }) + }); + + let address = f.ptr_address().0; + + if let Some(hot_functions) = HOT_FUNCTIONS.get() { + if hot_functions.contains(&address) { + IS_STALE.store(true, atomic::Ordering::Relaxed); + } + } else { + let _ = HOT_FUNCTIONS_PENDING + .lock() + .expect("Lock hot functions") + .insert(address); + } + + f.call(()) } pub fn on_hotpatch(f: impl Fn() + Send + Sync + 'static) { cargo_hot::subsecond::register_handler(Arc::new(f)); } + pub fn is_stale() -> bool { + IS_STALE.load(atomic::Ordering::Relaxed) + } + fn span(span: span::Stage) -> Span { log(client::Event::SpanStarted(span.clone())); @@ -428,4 +473,8 @@ mod internal { } pub fn on_hotpatch(_f: impl Fn() + Send + Sync + 'static) {} + + pub fn is_stale() -> bool { + false + } } diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index e4c2e4d4..45409267 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -343,21 +343,30 @@ where themer(derive_theme(), Element::from(mode).map(Event::Message)) }); - let notification = self.show_notification.then(|| { - themer( - derive_theme(), - bottom_right(opaque( - container(text("Press F12 to open debug metrics")) - .padding(10) - .style(container::dark), - )), - ) - }); + let notification = self + .show_notification + .then(|| text("Press F12 to open debug metrics")) + .or_else(|| { + debug::is_stale().then(|| { + text( + "Types have changed. Restart to re-enable hotpatching.", + ) + }) + }); stack![view] .height(Fill) .push_maybe(mode.map(opaque)) - .push_maybe(notification) + .push_maybe(notification.map(|notification| { + themer( + derive_theme(), + bottom_right(opaque( + container(notification) + .padding(10) + .style(container::dark), + )), + ) + })) .into() } From e689ec13a6a7b2fd0401b9f1f7152e262c903964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 24 Jun 2025 17:37:08 +0200 Subject: [PATCH 4/4] Place hot reloading behind `hot` feature flag --- Cargo.toml | 4 +- debug/Cargo.toml | 3 +- debug/src/lib.rs | 138 ++++++++++++++++++++++++++--------------------- 3 files changed, 81 insertions(+), 64 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bd7caad1..c9a3eea0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,10 +41,12 @@ qr_code = ["iced_widget/qr_code"] markdown = ["iced_widget/markdown"] # Enables lazy widgets lazy = ["iced_widget/lazy"] -# Enables a debug view in native platforms (press F12) +# Enables debug metrics in native platforms (press F12) debug = ["iced_winit/debug", "iced_devtools"] # Enables time-travel debugging (very experimental!) time-travel = ["debug", "iced_devtools/time-travel"] +# Enables hot reloading (very experimental!) +hot = ["debug", "iced_debug/hot"] # Enables the `thread-pool` futures executor as the `executor::Default` on native platforms thread-pool = ["iced_futures/thread-pool"] # Enables `tokio` as the `executor::Default` on native platforms diff --git a/debug/Cargo.toml b/debug/Cargo.toml index a9ffcf38..1c0abda3 100644 --- a/debug/Cargo.toml +++ b/debug/Cargo.toml @@ -11,7 +11,8 @@ categories.workspace = true keywords.workspace = true [features] -enable = ["dep:iced_beacon", "dep:cargo-hot"] +enable = ["dep:iced_beacon"] +hot = ["enable", "dep:cargo-hot"] [dependencies] iced_core.workspace = true diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 99def674..f79dde55 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -39,6 +39,7 @@ pub fn disable() { pub fn init(metadata: Metadata) { internal::init(metadata); + hot::init(); } pub fn quit() -> bool { @@ -114,15 +115,15 @@ pub fn commands() -> Subscription { } pub fn hot(f: impl FnOnce() -> O) -> O { - internal::hot(f) + hot::call(f) } pub fn on_hotpatch(f: impl Fn() + Send + Sync + 'static) { - internal::on_hotpatch(f) + hot::on_hotpatch(f) } pub fn is_stale() -> bool { - internal::is_stale() + hot::is_stale() } #[cfg(all(feature = "enable", not(target_arch = "wasm32")))] @@ -140,16 +141,8 @@ mod internal { use beacon::span; use beacon::span::present; - use std::collections::BTreeSet; use std::sync::atomic::{self, AtomicBool, AtomicUsize}; - use std::sync::{Arc, LazyLock, Mutex, OnceLock, RwLock}; - - static IS_STALE: AtomicBool = AtomicBool::new(false); - - static HOT_FUNCTIONS_PENDING: Mutex> = - Mutex::new(BTreeSet::new()); - - static HOT_FUNCTIONS: OnceLock> = OnceLock::new(); + use std::sync::{LazyLock, RwLock}; pub fn init(metadata: Metadata) { let name = metadata.name.split("::").next().unwrap_or(metadata.name); @@ -160,22 +153,6 @@ mod internal { theme: metadata.theme, can_time_travel: metadata.can_time_travel, }; - - cargo_hot::connect(); - - cargo_hot::subsecond::register_handler(Arc::new(|| { - if HOT_FUNCTIONS.get().is_none() { - HOT_FUNCTIONS - .set(std::mem::take( - &mut HOT_FUNCTIONS_PENDING - .lock() - .expect("Lock hot functions"), - )) - .expect("Set hot functions"); - } - - IS_STALE.store(false, atomic::Ordering::Relaxed); - })); } pub fn quit() -> bool { @@ -307,39 +284,6 @@ mod internal { Subscription::run(listen_for_commands) } - pub fn hot(f: impl FnOnce() -> O) -> O { - let mut f = Some(f); - - // The `move` here is important. Hotpatching will not work - // otherwise. - let mut f = cargo_hot::subsecond::HotFn::current(move || { - f.take().expect("Hot function is stale")() - }); - - let address = f.ptr_address().0; - - if let Some(hot_functions) = HOT_FUNCTIONS.get() { - if hot_functions.contains(&address) { - IS_STALE.store(true, atomic::Ordering::Relaxed); - } - } else { - let _ = HOT_FUNCTIONS_PENDING - .lock() - .expect("Lock hot functions") - .insert(address); - } - - f.call(()) - } - - pub fn on_hotpatch(f: impl Fn() + Send + Sync + 'static) { - cargo_hot::subsecond::register_handler(Arc::new(f)); - } - - pub fn is_stale() -> bool { - IS_STALE.load(atomic::Ordering::Relaxed) - } - fn span(span: span::Stage) -> Span { log(client::Event::SpanStarted(span.clone())); @@ -467,8 +411,78 @@ mod internal { impl Span { pub fn finish(self) {} } +} - pub fn hot(f: impl FnOnce() -> O) -> O { +#[cfg(feature = "hot")] +mod hot { + use std::collections::BTreeSet; + use std::sync::atomic::{self, AtomicBool}; + use std::sync::{Arc, Mutex, OnceLock}; + + static IS_STALE: AtomicBool = AtomicBool::new(false); + + static HOT_FUNCTIONS_PENDING: Mutex> = + Mutex::new(BTreeSet::new()); + + static HOT_FUNCTIONS: OnceLock> = OnceLock::new(); + + pub fn init() { + cargo_hot::connect(); + + cargo_hot::subsecond::register_handler(Arc::new(|| { + if HOT_FUNCTIONS.get().is_none() { + HOT_FUNCTIONS + .set(std::mem::take( + &mut HOT_FUNCTIONS_PENDING + .lock() + .expect("Lock hot functions"), + )) + .expect("Set hot functions"); + } + + IS_STALE.store(false, atomic::Ordering::Relaxed); + })); + } + + pub fn call(f: impl FnOnce() -> O) -> O { + let mut f = Some(f); + + // The `move` here is important. Hotpatching will not work + // otherwise. + let mut f = cargo_hot::subsecond::HotFn::current(move || { + f.take().expect("Hot function is stale")() + }); + + let address = f.ptr_address().0; + + if let Some(hot_functions) = HOT_FUNCTIONS.get() { + if hot_functions.contains(&address) { + IS_STALE.store(true, atomic::Ordering::Relaxed); + } + } else { + let _ = HOT_FUNCTIONS_PENDING + .lock() + .expect("Lock hot functions") + .insert(address); + } + + f.call(()) + } + + pub fn on_hotpatch(f: impl Fn() + Send + Sync + 'static) { + cargo_hot::subsecond::register_handler(Arc::new(f)); + } + + pub fn is_stale() -> bool { + IS_STALE.load(atomic::Ordering::Relaxed) + } +} + +#[cfg(not(feature = "hot"))] +mod hot { + pub fn init() {} + + pub fn call(f: impl FnOnce() -> O) -> O { f() }