Skip to main content

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