zng_ext_hot_reload/
lib.rs

1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3//!
4//! Hot reload service.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12mod cargo;
13mod node;
14mod util;
15use std::{
16    collections::{HashMap, HashSet},
17    fmt, io, mem,
18    path::PathBuf,
19    sync::Arc,
20    time::Duration,
21};
22
23pub use cargo::BuildError;
24use node::*;
25
26use zng_app::{
27    APP, AppExtension, DInstant, INSTANT,
28    event::{event, event_args},
29    handler::async_clmv,
30    update::UPDATES,
31};
32use zng_app_context::{LocalContext, app_local};
33use zng_ext_fs_watcher::WATCHER;
34pub use zng_ext_hot_reload_proc_macros::hot_node;
35use zng_task::{SignalOnce, parking_lot::Mutex};
36use zng_txt::Txt;
37use zng_unique_id::hot_reload::HOT_STATICS;
38use zng_unit::TimeUnits as _;
39use zng_var::{ResponseVar, Var};
40
41#[doc(inline)]
42pub use zng_unique_id::{hot_static, hot_static_ref, lazy_static};
43
44/// Declare hot reload entry.
45///
46/// Must be called at the root of the crate.
47///
48/// # Safety
49///
50/// Must be called only once at the hot-reload crate.
51#[macro_export]
52macro_rules! zng_hot_entry {
53    () => {
54        #[doc(hidden)] // used by proc-macro
55        pub use $crate::zng_hot_entry;
56
57        #[unsafe(no_mangle)] // SAFETY: docs instruct users to call the macro only once, name is unlikely to have collisions.
58        #[doc(hidden)] // used by lib loader
59        pub extern "C" fn zng_hot_entry(
60            manifest_dir: &&str,
61            node_name: &&'static str,
62            ctx: &mut $crate::zng_hot_entry::LocalContext,
63            exchange: &mut $crate::HotEntryExchange,
64        ) {
65            $crate::zng_hot_entry::entry(manifest_dir, node_name, ctx, exchange)
66        }
67
68        #[unsafe(no_mangle)] // SAFETY: docs instruct users to call the macro only once, name is unlikely to have collisions.
69        #[doc(hidden)]
70        pub extern "C" fn zng_hot_entry_init(patch: &$crate::StaticPatch) {
71            $crate::zng_hot_entry::init(patch)
72        }
73    };
74}
75
76#[doc(hidden)]
77pub mod zng_hot_entry {
78    pub use linkme as __linkme;
79
80    pub use crate::node::{HotNode, HotNodeArgs, HotNodeHost};
81    use crate::{HotEntryExchange, StaticPatch};
82    pub use zng_app_context::LocalContext;
83
84    pub struct HotNodeEntry {
85        pub manifest_dir: &'static str,
86        pub hot_node_name: &'static str,
87        pub hot_node_fn: fn(HotNodeArgs) -> HotNode,
88    }
89
90    #[linkme::distributed_slice]
91    pub static HOT_NODES: [HotNodeEntry];
92
93    pub fn entry(manifest_dir: &str, node_name: &'static str, ctx: &mut LocalContext, exchange: &mut HotEntryExchange) {
94        for entry in HOT_NODES.iter() {
95            if node_name == entry.hot_node_name && manifest_dir == entry.manifest_dir {
96                let args = match std::mem::replace(exchange, HotEntryExchange::Responding) {
97                    HotEntryExchange::Request(args) => args,
98                    _ => panic!("bad request"),
99                };
100                let node = ctx.with_context(|| (entry.hot_node_fn)(args));
101                *exchange = HotEntryExchange::Response(Some(node));
102                return;
103            }
104        }
105        *exchange = HotEntryExchange::Response(None);
106    }
107
108    pub fn init(statics: &StaticPatch) {
109        std::panic::set_hook(Box::new(|args| {
110            eprintln!("PANIC IN HOT LOADED LIBRARY, ABORTING");
111            crate::util::crash_handler(args);
112            zng_env::exit(101);
113        }));
114
115        // SAFETY: hot reload rebuilds in the same environment, so this is safe if the keys are strong enough.
116        unsafe { statics.apply() }
117    }
118}
119
120type StaticPatchersMap = HashMap<&'static dyn zng_unique_id::hot_reload::PatchKey, unsafe fn(*const ()) -> *const ()>;
121
122#[doc(hidden)]
123#[derive(Clone)]
124#[repr(C)]
125pub struct StaticPatch {
126    tracing: tracing_shared::SharedLogger,
127    entries: Arc<StaticPatchersMap>,
128}
129impl StaticPatch {
130    /// Called on the static code (host).
131    pub fn capture() -> Self {
132        let mut entries = StaticPatchersMap::with_capacity(HOT_STATICS.len());
133        for (key, val) in HOT_STATICS.iter() {
134            match entries.entry(*key) {
135                std::collections::hash_map::Entry::Vacant(e) => {
136                    e.insert(*val);
137                }
138                std::collections::hash_map::Entry::Occupied(_) => {
139                    panic!("repeated hot static key `{key:?}`");
140                }
141            }
142        }
143
144        Self {
145            entries: Arc::new(entries),
146            tracing: tracing_shared::SharedLogger::new(),
147        }
148    }
149
150    /// Called on the dynamic code (dylib).
151    unsafe fn apply(&self) {
152        self.tracing.install();
153
154        for (key, patch) in HOT_STATICS.iter() {
155            if let Some(val) = self.entries.get(key) {
156                // println!("patched `{key:?}`");
157                // SAFETY: HOT_STATICS is defined using linkme, so all entries are defined by the hot_static! macro
158                unsafe {
159                    patch(val(std::ptr::null()));
160                }
161            } else {
162                eprintln!("did not find `{key:?}` to patch, static references may fail");
163            }
164        }
165    }
166}
167
168/// Status of a monitored dynamic library crate.
169#[derive(Clone, PartialEq, Debug)]
170#[non_exhaustive]
171pub struct HotStatus {
172    /// Dynamic library crate directory.
173    ///
174    /// Any file changes inside this directory triggers a rebuild.
175    pub manifest_dir: Txt,
176
177    /// Build start time if is rebuilding.
178    pub building: Option<DInstant>,
179
180    /// Last rebuild and reload result.
181    ///
182    /// is `Ok(build_duration)` or `Err(build_error)`.
183    pub last_build: Result<Duration, BuildError>,
184
185    /// Number of times the dynamically library was rebuilt (successfully or with error).
186    pub rebuild_count: usize,
187}
188impl HotStatus {
189    /// Gets the build time if the last build succeeded.
190    pub fn ok(&self) -> Option<Duration> {
191        self.last_build.as_ref().ok().copied()
192    }
193
194    /// If the last build was cancelled.
195    pub fn is_cancelled(&self) -> bool {
196        matches!(&self.last_build, Err(BuildError::Cancelled))
197    }
198
199    /// Gets the last build error if it failed and was not cancelled.
200    pub fn err(&self) -> Option<&BuildError> {
201        self.last_build.as_ref().err().filter(|e| !matches!(e, BuildError::Cancelled))
202    }
203}
204
205/// Hot reload app extension.
206///
207/// # Events
208///
209/// Events this extension provides.
210///
211/// * [`HOT_RELOAD_EVENT`]
212///
213/// # Services
214///
215/// Services this extension provides.
216///
217/// * [`HOT_RELOAD`]
218#[derive(Default)]
219pub struct HotReloadManager {
220    libs: HashMap<&'static str, WatchedLib>,
221    static_patch: Option<StaticPatch>,
222}
223impl AppExtension for HotReloadManager {
224    fn init(&mut self) {
225        // watch all hot libraries.
226        let mut status = vec![];
227        for entry in crate::zng_hot_entry::HOT_NODES.iter() {
228            if let std::collections::hash_map::Entry::Vacant(e) = self.libs.entry(entry.manifest_dir) {
229                e.insert(WatchedLib::default());
230                WATCHER.watch_dir(entry.manifest_dir, true).perm();
231
232                status.push(HotStatus {
233                    manifest_dir: entry.manifest_dir.into(),
234                    building: None,
235                    last_build: Ok(Duration::MAX),
236                    rebuild_count: 0,
237                });
238            }
239        }
240        HOT_RELOAD_SV.read().status.set(status);
241    }
242
243    fn event_preview(&mut self, update: &mut zng_app::update::EventUpdate) {
244        if let Some(args) = zng_ext_fs_watcher::FS_CHANGES_EVENT.on(update) {
245            for (manifest_dir, watched) in self.libs.iter_mut() {
246                if args.changes_for_path(manifest_dir.as_ref()).next().is_some() {
247                    watched.rebuild((*manifest_dir).into(), self.static_patch.get_or_insert_with(StaticPatch::capture));
248                }
249            }
250        }
251    }
252
253    fn update_preview(&mut self) {
254        for (manifest_dir, watched) in self.libs.iter_mut() {
255            if let Some(b) = &watched.building
256                && let Some(r) = b.rebuild_load.rsp()
257            {
258                let build_time = b.start_time.elapsed();
259                let mut lib = None;
260                let status_r = match r {
261                    Ok(l) => {
262                        lib = Some(l);
263                        Ok(build_time)
264                    }
265                    Err(e) => {
266                        if matches!(&e, BuildError::Cancelled) {
267                            tracing::warn!("cancelled rebuild `{manifest_dir}`");
268                        } else {
269                            tracing::error!("failed rebuild `{manifest_dir}`, {e}");
270                        }
271                        Err(e)
272                    }
273                };
274                if let Some(lib) = lib {
275                    tracing::info!("rebuilt and reloaded `{manifest_dir}` in {build_time:?}");
276                    HOT_RELOAD.set(lib.clone());
277                    HOT_RELOAD_EVENT.notify(HotReloadArgs::now(lib));
278                }
279
280                watched.building = None;
281
282                let manifest_dir = *manifest_dir;
283                HOT_RELOAD_SV.read().status.modify(move |s| {
284                    let s = s.iter_mut().find(|s| s.manifest_dir == manifest_dir).unwrap();
285                    s.building = None;
286                    s.last_build = status_r;
287                    s.rebuild_count += 1;
288                });
289
290                if mem::take(&mut watched.rebuild_again) {
291                    HOT_RELOAD_SV.write().rebuild_requests.push(manifest_dir.into());
292                }
293            }
294        }
295
296        let mut sv = HOT_RELOAD_SV.write();
297        let requests: HashSet<Txt> = sv.cancel_requests.drain(..).collect();
298        for r in requests {
299            if let Some(watched) = self.libs.get_mut(r.as_str())
300                && let Some(b) = &watched.building
301            {
302                b.cancel_build.set();
303            }
304        }
305
306        let requests: HashSet<Txt> = sv.rebuild_requests.drain(..).collect();
307        drop(sv);
308        for r in requests {
309            if let Some(watched) = self.libs.get_mut(r.as_str()) {
310                watched.rebuild(r, self.static_patch.get_or_insert_with(StaticPatch::capture));
311            } else {
312                tracing::error!("cannot rebuild `{r}`, unknown");
313            }
314        }
315    }
316}
317
318type RebuildVar = ResponseVar<Result<PathBuf, BuildError>>;
319
320type RebuildLoadVar = ResponseVar<Result<HotLib, BuildError>>;
321
322/// Arguments for custom rebuild runners.
323///
324/// See [`HOT_RELOAD.rebuilder`] for more details.
325///
326/// [`HOT_RELOAD.rebuilder`]: HOT_RELOAD::rebuilder
327#[derive(Clone, Debug, PartialEq)]
328#[non_exhaustive]
329pub struct BuildArgs {
330    /// Crate that changed.
331    pub manifest_dir: Txt,
332    /// Cancel signal.
333    ///
334    /// If the build cannot be cancelled or has already finished this signal must be ignored and
335    /// the normal result returned.
336    pub cancel_build: SignalOnce,
337}
338impl BuildArgs {
339    /// Calls `cargo build [--package {package}] --message-format json` and cancels it as soon as the dylib is rebuilt.
340    ///
341    /// Always returns `Some(_)`.
342    pub fn build(&self, package: Option<&str>) -> Option<RebuildVar> {
343        Some(cargo::build(
344            &self.manifest_dir,
345            "--package",
346            package.unwrap_or(""),
347            "",
348            "",
349            self.cancel_build.clone(),
350        ))
351    }
352
353    /// Calls `cargo build [--package {package}] --example {example} --message-format json` and cancels
354    /// it as soon as the dylib is rebuilt.
355    ///
356    /// Always returns `Some(_)`.
357    pub fn build_example(&self, package: Option<&str>, example: &str) -> Option<RebuildVar> {
358        Some(cargo::build(
359            &self.manifest_dir,
360            "--package",
361            package.unwrap_or(""),
362            "--example",
363            example,
364            self.cancel_build.clone(),
365        ))
366    }
367
368    /// Calls `cargo build [--package {package}] --bin {bin}  --message-format json` and cancels it as
369    /// soon as the dylib is rebuilt.
370    ///
371    /// Always returns `Some(_)`.
372    pub fn build_bin(&self, package: Option<&str>, bin: &str) -> Option<RebuildVar> {
373        Some(cargo::build(
374            &self.manifest_dir,
375            "--package",
376            package.unwrap_or(""),
377            "--bin",
378            bin,
379            self.cancel_build.clone(),
380        ))
381    }
382
383    /// Calls `cargo build --manifest-path {path} --message-format json` and cancels it as soon as the dylib is rebuilt.
384    ///
385    /// Always returns `Some(_)`.
386    pub fn build_manifest(&self, path: &str) -> Option<RebuildVar> {
387        Some(cargo::build(
388            &self.manifest_dir,
389            "--manifest-path",
390            path,
391            "",
392            "",
393            self.cancel_build.clone(),
394        ))
395    }
396
397    /// Calls a custom command that must write to stdout the same way `cargo build --message-format json` does.
398    ///
399    /// The command will run until it writes the `"compiler-artifact"` for the `manifest_dir/Cargo.toml` to stdout, it will
400    /// then be killed.
401    ///
402    /// Always returns `Some(_)`.
403    pub fn custom(&self, cmd: std::process::Command) -> Option<RebuildVar> {
404        Some(cargo::build_custom(&self.manifest_dir, cmd, self.cancel_build.clone()))
405    }
406
407    /// Call a custom command defined in an environment var.
408    ///
409    /// The variable value must be arguments for `cargo`, that is `cargo $VAR`.
410    ///
411    /// See [`custom`] for other requirements of the command.
412    ///
413    /// If `var_key` is empty the default key `"ZNG_HOT_RELOAD_REBUILDER"` is used.
414    ///
415    /// Returns `None` if the var is not found or is set empty.
416    ///
417    /// [`custom`]: Self::custom
418    pub fn custom_env(&self, mut var_key: &str) -> Option<RebuildVar> {
419        if var_key.is_empty() {
420            var_key = "ZNG_HOT_RELOAD_REBUILDER";
421        }
422
423        let custom = std::env::var(var_key).ok()?;
424        let mut custom = custom.split(' ');
425
426        let subcommand = custom.next()?;
427
428        let mut cmd = std::process::Command::new("cargo");
429        cmd.arg(subcommand);
430        cmd.args(custom);
431
432        self.custom(cmd)
433    }
434
435    /// The default action.
436    ///
437    /// Tries `custom_env`, if env is not set, does `build(None)`.
438    ///
439    /// Always returns `Some(_)`.
440    pub fn default_build(&self) -> Option<RebuildVar> {
441        self.custom_env("").or_else(|| self.build(None))
442    }
443}
444
445/// Hot reload service.
446///
447/// # Provider
448///
449/// This service is provided by the [`HotReloadManager`] extension, it will panic if used in an app not extended.
450#[expect(non_camel_case_types)]
451pub struct HOT_RELOAD;
452impl HOT_RELOAD {
453    /// Hot reload status, libs that are rebuilding, errors.
454    pub fn status(&self) -> Var<Vec<HotStatus>> {
455        HOT_RELOAD_SV.read().status.read_only()
456    }
457
458    /// Register a handler that can override the hot library rebuild.
459    ///
460    /// The command should rebuild using the same features used to run the program (not just rebuild the dylib).
461    /// By default it is just `cargo build`, that works if the program was started using only `cargo run`, but
462    /// an example program needs a custom runner.
463    ///
464    /// If `rebuilder` wants to handle the rebuild it must return a response var that updates when the rebuild is finished with
465    /// the path to the rebuilt dylib. The [`BuildArgs`] also provides helper methods to rebuild common workspace setups.
466    ///
467    /// Note that unlike most services the `rebuilder` is registered immediately, not after an update cycle.
468    pub fn rebuilder(&self, rebuilder: impl FnMut(BuildArgs) -> Option<RebuildVar> + Send + 'static) {
469        HOT_RELOAD_SV.write().rebuilders.get_mut().push(Box::new(rebuilder));
470    }
471
472    /// Request a rebuild, if `manifest_dir` is a hot library.
473    ///
474    /// Note that changes inside the directory already trigger a rebuild automatically.
475    pub fn rebuild(&self, manifest_dir: impl Into<Txt>) {
476        HOT_RELOAD_SV.write().rebuild_requests.push(manifest_dir.into());
477        UPDATES.update(None);
478    }
479
480    /// Request a rebuild cancel for the current building `manifest_dir`.
481    pub fn cancel(&self, manifest_dir: impl Into<Txt>) {
482        HOT_RELOAD_SV.write().cancel_requests.push(manifest_dir.into());
483        UPDATES.update(None);
484    }
485
486    pub(crate) fn lib(&self, manifest_dir: &'static str) -> Option<HotLib> {
487        HOT_RELOAD_SV
488            .read()
489            .libs
490            .iter()
491            .rev()
492            .find(|l| l.manifest_dir() == manifest_dir)
493            .cloned()
494    }
495
496    fn set(&self, lib: HotLib) {
497        // we never unload HotLib because hot nodes can pass &'static references (usually inside `Txt`) to the
498        // program that will remain being used after.
499        HOT_RELOAD_SV.write().libs.push(lib);
500    }
501}
502app_local! {
503    static HOT_RELOAD_SV: HotReloadService = {
504        APP.extensions().require::<HotReloadManager>();
505        HotReloadService {
506            libs: vec![],
507            rebuilders: Mutex::new(vec![]),
508            status: zng_var::var(vec![]),
509            rebuild_requests: vec![],
510            cancel_requests: vec![],
511        }
512    };
513}
514struct HotReloadService {
515    libs: Vec<HotLib>,
516    // mutex for Sync only
517    #[expect(clippy::type_complexity)]
518    rebuilders: Mutex<Vec<Box<dyn FnMut(BuildArgs) -> Option<RebuildVar> + Send + 'static>>>,
519
520    status: Var<Vec<HotStatus>>,
521    rebuild_requests: Vec<Txt>,
522    cancel_requests: Vec<Txt>,
523}
524impl HotReloadService {
525    fn rebuild_reload(&mut self, manifest_dir: Txt, static_patch: &StaticPatch) -> (RebuildLoadVar, SignalOnce) {
526        let (rebuild, cancel) = self.rebuild(manifest_dir.clone());
527        let rebuild_load = zng_task::respond(async_clmv!(static_patch, {
528            let build_path = rebuild.wait_rsp().await?;
529
530            // copy dylib to not block the next rebuild
531            let file_name = match build_path.file_name() {
532                Some(f) => f.to_string_lossy(),
533                None => return Err(std::io::Error::new(std::io::ErrorKind::NotFound, "dylib path does not have a file name").into()),
534            };
535
536            // cleanup previous session
537            for p in glob::glob(&format!("{}/zng-hot-{file_name}-*", build_path.parent().unwrap().display()))
538                .unwrap()
539                .flatten()
540            {
541                let _ = std::fs::remove_file(p);
542            }
543
544            let mut unique_path = build_path.clone();
545            let ts = std::time::SystemTime::now()
546                .duration_since(std::time::UNIX_EPOCH)
547                .unwrap()
548                .as_millis();
549            unique_path.set_file_name(format!("zng-hot-{file_name}-{ts:x}"));
550            std::fs::copy(&build_path, &unique_path)?;
551
552            let dylib = zng_task::wait(move || HotLib::new(&static_patch, manifest_dir, unique_path));
553            match zng_task::with_deadline(dylib, 10.secs()).await {
554                Ok(r) => r.map_err(Into::into),
555                Err(_) => Err(BuildError::Io(Arc::new(io::Error::new(
556                    io::ErrorKind::TimedOut,
557                    "hot dylib did not init after 10s",
558                )))),
559            }
560        }));
561        (rebuild_load, cancel)
562    }
563
564    fn rebuild(&mut self, manifest_dir: Txt) -> (RebuildVar, SignalOnce) {
565        for r in self.rebuilders.get_mut() {
566            let cancel = SignalOnce::new();
567            let args = BuildArgs {
568                manifest_dir: manifest_dir.clone(),
569                cancel_build: cancel.clone(),
570            };
571            if let Some(r) = r(args.clone()) {
572                return (r, cancel);
573            }
574        }
575        let cancel = SignalOnce::new();
576        let args = BuildArgs {
577            manifest_dir: manifest_dir.clone(),
578            cancel_build: cancel.clone(),
579        };
580        (args.default_build().unwrap(), cancel)
581    }
582}
583
584event_args! {
585    /// Args for [`HOT_RELOAD_EVENT`].
586    pub struct HotReloadArgs {
587        /// Reloaded library.
588        pub(crate) lib: HotLib,
589
590        ..
591
592        fn delivery_list(&self, list: &mut UpdateDeliveryList) {
593            list.search_all();
594        }
595    }
596}
597impl HotReloadArgs {
598    /// Crate directory that changed and caused the rebuild.
599    pub fn manifest_dir(&self) -> &Txt {
600        self.lib.manifest_dir()
601    }
602}
603
604event! {
605    /// Event notifies when a new version of a hot reload dynamic library has finished rebuild and has loaded.
606    ///
607    /// This event is used internally by hot nodes to reinit.
608    pub static HOT_RELOAD_EVENT: HotReloadArgs;
609}
610
611#[derive(Default)]
612struct WatchedLib {
613    building: Option<BuildingLib>,
614    rebuild_again: bool,
615}
616impl WatchedLib {
617    fn rebuild(&mut self, manifest_dir: Txt, static_path: &StaticPatch) {
618        if let Some(b) = &self.building {
619            if b.start_time.elapsed() > WATCHER.debounce().get() + 34.ms() {
620                // WATCHER debounce notifies immediately, then debounces. Some
621                // IDEs (VsCode) touch the saving file multiple times within
622                // the debounce interval, this causes two rebuild requests.
623                //
624                // So we only cancel rebuild if the second event (current) is not
625                // within debounce + a generous 34ms for the notification delay.
626                b.cancel_build.set();
627                self.rebuild_again = true;
628            }
629        } else {
630            let start_time = INSTANT.now();
631            tracing::info!("rebuilding `{manifest_dir}`");
632
633            let mut sv = HOT_RELOAD_SV.write();
634
635            let (rebuild_load, cancel_build) = sv.rebuild_reload(manifest_dir.clone(), static_path);
636            self.building = Some(BuildingLib {
637                start_time,
638                rebuild_load,
639                cancel_build,
640            });
641
642            sv.status.modify(move |s| {
643                s.iter_mut().find(|s| s.manifest_dir == manifest_dir).unwrap().building = Some(start_time);
644            });
645        }
646    }
647}
648
649struct BuildingLib {
650    start_time: DInstant,
651    rebuild_load: RebuildLoadVar,
652    cancel_build: SignalOnce,
653}
654
655#[doc(hidden)]
656pub enum HotEntryExchange {
657    Request(HotNodeArgs),
658    Responding,
659    Response(Option<HotNode>),
660}
661
662/// Dynamically loaded library.
663#[derive(Clone)]
664pub(crate) struct HotLib {
665    manifest_dir: Txt,
666    lib: Arc<libloading::Library>,
667    hot_entry: unsafe extern "C" fn(&&str, &&'static str, &mut LocalContext, &mut HotEntryExchange),
668}
669impl PartialEq for HotLib {
670    fn eq(&self, other: &Self) -> bool {
671        Arc::ptr_eq(&self.lib, &other.lib)
672    }
673}
674impl fmt::Debug for HotLib {
675    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
676        f.debug_struct("HotLib")
677            .field("manifest_dir", &self.manifest_dir)
678            .finish_non_exhaustive()
679    }
680}
681impl HotLib {
682    pub fn new(patch: &StaticPatch, manifest_dir: Txt, lib: impl AsRef<std::ffi::OsStr>) -> Result<Self, libloading::Error> {
683        unsafe {
684            // SAFETY: assuming the hot lib was setup as the docs instruct, this works,
685            // even the `linkme` stuff does not require any special care.
686            //
687            // If the hot lib developer add some "ctor/dtor" stuff and that fails they will probably
688            // know why, hot reloading should only run in dev machines.
689            let lib = libloading::Library::new(lib.as_ref())?;
690
691            // SAFETY: thats the signature.
692            let init: unsafe extern "C" fn(&StaticPatch) = *lib.get(b"zng_hot_entry_init")?;
693            init(patch);
694
695            Ok(Self {
696                manifest_dir,
697                hot_entry: *lib.get(b"zng_hot_entry")?,
698                lib: Arc::new(lib),
699            })
700        }
701    }
702
703    /// Lib identifier.
704    pub fn manifest_dir(&self) -> &Txt {
705        &self.manifest_dir
706    }
707
708    pub fn instantiate(&self, hot_node_name: &'static str, ctx: &mut LocalContext, args: HotNodeArgs) -> Option<HotNode> {
709        let mut exchange = HotEntryExchange::Request(args);
710        // SAFETY: lib is still loaded and will remain until all HotNodes are dropped.
711        unsafe { (self.hot_entry)(&self.manifest_dir.as_str(), &hot_node_name, ctx, &mut exchange) };
712        let mut node = match exchange {
713            HotEntryExchange::Response(n) => n,
714            _ => None,
715        };
716        if let Some(n) = &mut node {
717            n._lib = Some(self.lib.clone());
718        }
719        node
720    }
721}