zng_ext_hot_reload/
lib.rs

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