Hot-reload (experimental)
Status: Experimental, debug-only. Currently driven by Dioxus’s
dxCLI through its--hot-patchmode. The patch points and the websocket client that talks to dx’s dev-server both live insideblinc_app— Blinc apps don’t depend on Dioxus at runtime. A nativeblinc devdriver that vendors the dev-server side too is on the roadmap (issue #30, level 2).
Blinc can hot-patch the body of your UI builder closure while the
app is running, so iterating on a layout or styling tweak doesn’t
need a full rebuild + relaunch. State held by Stateful widgets
and hooks (use_state, use_fsm, …) is preserved across
patches because Blinc keys it by InstanceKey rather than by
closure identity — your reactive graph survives the swap.
The integration uses the subsecond
crate. subsecond is itself gated on debug_assertions, so even if
you ship a release binary with the hot-reload feature enabled, the
patch points compile down to direct calls and there is zero runtime
overhead.
When you enable the hot-reload feature, Blinc also pulls in
tungstenite (websocket
client, no TLS — dx serves over ws://localhost) and
dioxus-cli-config
(tiny env-var convention crate, zero deps on native). Both stay
out of default builds; they only show up when the feature is on.
Setup
-
Enable the feature on
blinc_app:[dependencies] blinc_app = { version = "0.5", features = ["windowed", "hot-reload"] } -
Install the Dioxus CLI (one-time):
cargo install dioxus-cli -
Run your binary crate under
dx:dx serve --hot-patch \ --package my_app \ --features blinc_app/hot-reload \ --platform macosdxbuilds your binary, launches it, and watches the source tree. The Blinc runtime opens a websocket back to dx as soon as the window is ready (announcing the running process’s ASLR offset, build id, and pid as query parameters), so dx can compute jump-table offsets when it ships a patch. When dx detects a change in the binary crate it compiles a patch, ships it over the websocket, andsubsecond::apply_patchapplies it in place. The next frame Blinc renders picks up the new closure body.
The websocket protocol matches
dxCLI 0.7+. Blinc keeps a small mirror of the relevantDevserverMsgsubset locally; if a future dx release changes the schema for the fields we read (jump_table,for_pid), thehot-reloadfeature will need a compatibility bump. Other fields (template patches, asset reloads, etc.) deserialise opaquely so dx can add new fields without breaking us.
What gets hot-reloaded
- Code in the binary crate — i.e. the crate where
main.rslives.subsecondonly patches the “tip” crate; this is a hard limitation of the dynamic-linking approach it uses. - CSS strings inlined in your UI builder. The patched closure
sees the new string literal and re-registers the stylesheet on
the rebuild that follows the patch. Blinc’s hot-reload runtime
clears the accumulated stylesheet, drops
css_sources, and resetsrebuild_countto zero before re-invoking the closure, so the common
guard fires again with the new content and the result is a fresh sheet — no stale rules from the pre-patch run.#![allow(unused)] fn main() { if ctx.rebuild_count == 0 { ctx.add_css(MY_CSS); } } - Runtime-loaded image and SVG assets, when you opt the
app’s asset directory into the file watcher:
The watcher tails the directory recursively. When a file under it changes, Blinc drops the matching entry from its image cache (matched by path-suffix against the URI you passed tofn main() -> blinc_app::Result<()> { #[cfg(feature = "hot-reload")] blinc_app::hot_reload::watch_dir("assets"); WindowedApp::run(WindowConfig::default(), |ctx| build_ui(ctx)) }image("...")), wipes the SVG document cache and atlas, and forces the next frame to re-read the bytes off disk. Works for PNG, JPEG, WebP, runtime-loaded fonts, and SVG sources loaded from a path. Does not fire forinclude_bytes!-embedded assets — those live in rodata that subsecond can’t touch (see What doesn’t below). - State held by
Statefulwidgets and hooks. Blinc’sInstanceKeyis derived from#[track_caller]+ a per-frame call counter, so the same logical widget gets the same key before and after a patch.use_state,use_fsm, reactive signals, and FSM state all survive.
What doesn’t
- Code in workspace dependencies (
blinc_layout,blinc_app,blinc_gpu, etc.). Editing the framework itself still requires a full rebuild. - CSS in
conststrings.subsecondpatches function bodies, not the binary’s read-only data segment. Aconst STYLESHEET: &str = "..."referenced from your UI builder will keep pointing at the original string after a patch, so edits to the const won’t be picked up. Inline the CSS directly in thectx.add_css(r#"..."#)call — that string literal lives in the patched function body, so subsecond rewrites it alongside the rest of the closure. Same reasoning applies tostaticstrings. include_bytes!/include_str!assets. Files baked into the binary at compile time live in the same rodata segment that CSS const strings do — subsecond can’t update them. Aregister_font(include_bytes!("Inter.ttf").to_vec())keeps serving the original font bytes after a patch. To get hot-reloadable font / image assets, load them via runtime path instead (image("assets/logo.png"),register_font(std::fs::read("Inter.ttf")?)) and put the containing dir underwatch_dir.- Static initialisers in the patched crate.
subsecondtracks statics across patches but does not re-run their destructors, and thread-locals get reset. If your binary crate initialises a heavyOnceLockat startup, that initialiser won’t re-run after a patch. - Struct field-layout changes.
subsecondcannot safely patch a closure if a struct it captures has had fields added, removed, or reordered — the in-memory layout has shifted out from under the patched code. In practice this means: change behaviour freely, but if you change a field on a struct that participates in your reactive graph (e.g. a custom#[derive(BlincComponent)]struct), restart. - Release builds.
subsecond::callis a no-op whendebug_assertionsis off. Release builds compile thehot-reloadfeature in but receive no patches at runtime.
Multi-window apps
The integration patches both the primary-window UI builder and any
secondary windows opened via WindowedApp::create_window(...).
Each window’s builder closure goes through its own subsecond::call
patch point, so a code change is picked up on the next frame of
every open window.
CLI: blinc dev (in development)
The future native driver lives at blinc dev. It accepts a --mode
flag to pick the compilation path:
blinc dev # default: --mode rust (subsecond)
blinc dev --mode rust # explicit Rust hot-patch mode
blinc dev --mode dsl # Blinc DSL via Zyntax (in plan)
Today both modes are stubs that print a friendly “not yet” message —
the Rust path waits on the websocket-driver work tracked under
issue #30 level 2; the DSL path waits on Zyntax Grammar2 + Runtime2.
For now, use dx serve --hot-patch (above) to drive Rust hot-patches.
Troubleshooting
- “Patch had no effect.” Double-check that the change is in
your binary crate, not in a workspace dependency.
dx serve --hot-patchprints which crate it patched; if the line doesn’t mention your binary, the edit was outside the patchable scope. - dx logs
Ignoring hotpatch since there is no ASLR reference. The app didn’t open the websocket back to dx, so dx couldn’t compute jump-table offsets. Check that you compiled the app with thehot-reloadfeature on (--features blinc_app/hot-reload) and that you’re running the resulting binary as a child ofdx serve --hot-patch(which sets theDIOXUS_DEVSERVER_*env vars the client reads). On startup the client logshot-reload: connectedat info level when the websocket opens. - CSS edit doesn’t show up. If your stylesheet lives in a
const STYLESHEET: &str = ...(or other binary-resident static), the patched closure references the pre-patch address and never sees the new string — see What doesn’t above. Inline the CSS in thectx.add_css(r#"..."#)call instead. Once the closure body owns the literal, edits round-trip through the patch without restart. - State got cleared after a patch. This means
subseconddetected a structural change (a captured struct’s fields moved) and forced a full re-instance. The next frame rebuilds from scratch. This is expected — restart and rebuild if you need the old state back. - Crash after a patch. Most likely a struct-layout edit that
squeaked past
subsecond’s safety checks. Restart the app to recover, then file an issue against Blinc with the diff that triggered it. Even if the root cause is insubsecond, the Blinc team wants to know which patterns are unsafe in practice so we can document them here.
Roadmap
Level 1 (shipped) installs the in-process patch points behind a
feature flag and the websocket client that talks to dx serve --hot-patch. Editing a UI body in your binary crate now triggers
an in-place patch without restart. Blinc apps don’t link any of
the Dioxus reactive runtime at runtime — only subsecond,
tungstenite, and the env-var convention crate.
Level 2 will vendor the dev-server side so blinc dev becomes a
first-party command — no dx install required, and a chance to
tighten the rebuild-detection rules to Blinc’s tree (e.g.
invalidate Stylesheet caches when CSS-only files change).