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