zng-view-prebuilt 0.10.4

Part of the zng project.
Documentation
#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo-icon.png")]
#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo.png")]
//!
//! Dynamically links to [`zng-view`] pre-built library.
//!
//! [`zng-view`]: https://docs.rs/zng-view
//!
//! # Crate
//!
#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
#![warn(unused_extern_crates)]
#![warn(missing_docs)]

use core::fmt;
use libloading::*;
use std::{env, io, path::PathBuf};
use zng_view_api::StaticPatch;

zng_env::on_process_start!(|_| {
    if std::env::var("ZNG_VIEW_NO_INIT_START").is_err() {
        view_process_main()
    }
});

/// Calls the prebuilt [`view_process_main`].
///
/// Note that this only needs to be called if the view-process is not built on the same executable, if
/// it is you only need to call [`zng_env::init!`] at the beginning of the executable main.
///
/// You can also disable start on init by setting the `ZNG_VIEW_NO_INIT_START` environment variable. In this
/// case you must manually call this function.
///
/// [`view_process_main`]: https://docs.rs/zng-view/fn.view_process_main.html
pub fn view_process_main() {
    ViewLib::install().unwrap().view_process_main()
}

/// Call the prebuilt [`run_same_process`].
///
/// This function exits the process after `run_app` returns.
///
/// [`run_same_process`]: https://docs.rs/zng-view/fn.run_same_process.html
///
/// # Panics
///
/// Panics if it fails to [install] the prebuilt binary.
///
/// # Aborts
///
/// Kills the process with code `101` if there is a panic generated by the pre-built code or by threads started by the pre-build code.
///
/// [install]: ViewLib::install
pub fn run_same_process(run_app: impl FnOnce() + Send + 'static) -> ! {
    ViewLib::install().unwrap().run_same_process(run_app)
}

/// Dynamically linked pre-built view.
pub struct ViewLib {
    view_process_main_fn: unsafe extern "C" fn(),
    run_same_process_fn: unsafe extern "C" fn(&StaticPatch, extern "C" fn()),
    _lib: Library,
}
impl ViewLib {
    /// Extract the embedded library to the temp directory and link to it.
    pub fn install() -> Result<Self, Error> {
        let dir = env::temp_dir().join("zng_view");
        std::fs::create_dir_all(&dir)?;
        Self::install_to(dir)
    }

    /// Try to delete the installed library from the temp directory.
    ///
    /// See [`uninstall_from`] for details.
    ///
    /// [`uninstall_from`]: Self::uninstall_from
    pub fn uninstall() -> Result<bool, io::Error> {
        let dir = env::temp_dir().join("zng_view");
        Self::uninstall_from(dir)
    }

    /// Extract the embedded library to `dir` and link to it.
    pub fn install_to(dir: impl Into<PathBuf>) -> Result<Self, Error> {
        Self::install_to_impl(dir.into())
    }
    fn install_to_impl(dir: PathBuf) -> Result<Self, Error> {
        #[cfg(not(zng_lib_embedded))]
        {
            let _ = dir;
            panic!("library not embedded");
        }

        #[cfg(zng_lib_embedded)]
        {
            let file = Self::install_path(dir);

            if !file.exists() {
                std::fs::write(&file, LIB)?;
            }

            Self::link(file)
        }
    }

    /// Try to delete the installed library from the given `dir`.
    ///
    /// Returns `Ok(true)` if uninstalled, `Ok(false)` if was not installed and `Err(_)`
    /// if is installed and failed to delete.
    ///
    /// Note that the file is probably in use if it was installed in the current process instance, in Windows
    /// files cannot be deleted until they are released.
    pub fn uninstall_from(dir: impl Into<PathBuf>) -> Result<bool, io::Error> {
        Self::uninstall_from_impl(dir.into())
    }
    fn uninstall_from_impl(dir: PathBuf) -> Result<bool, io::Error> {
        #[cfg(not(zng_lib_embedded))]
        {
            let _ = dir;
            Ok(false)
        }

        #[cfg(zng_lib_embedded)]
        {
            let file = Self::install_path(dir);

            if file.exists() {
                std::fs::remove_file(file)?;
                Ok(true)
            } else {
                Ok(false)
            }
        }
    }

    #[cfg(zng_lib_embedded)]
    fn install_path(dir: PathBuf) -> PathBuf {
        #[cfg(target_os = "windows")]
        let file_name = format!("{LIB_NAME}.dll");
        #[cfg(target_os = "linux")]
        let file_name = format!("{LIB_NAME}.so");
        #[cfg(target_os = "macos")]
        let file_name = format!("{LIB_NAME}.dylib");

        dir.join(file_name)
    }

    /// Link to the pre-built library file.
    ///
    /// If the file does not have an extension searches for a file without extension then a
    /// `.dll` file in Windows, a `.so` file in Linux and a `.dylib` file in other operating systems.
    ///
    /// Note that the is only searched as described above, if it is not found an error returns immediately,
    /// the operating system library search feature is not used.
    pub fn link(view_dylib: impl Into<PathBuf>) -> Result<Self, Error> {
        Self::link_impl(view_dylib.into())
    }
    fn link_impl(mut lib: PathBuf) -> Result<Self, Error> {
        if !lib.exists() && lib.extension().is_none() {
            #[cfg(target_os = "windows")]
            lib.set_extension("dll");
            #[cfg(target_os = "linux")]
            lib.set_extension("so");
            #[cfg(target_os = "macos")]
            lib.set_extension("dylib");
        }

        if lib.exists() {
            // this disables Windows DLL search feature.
            lib = dunce::canonicalize(lib)?;
        }

        if !lib.exists() {
            return Err(io::Error::new(io::ErrorKind::NotFound, format!("view library not found in `{}`", lib.display())).into());
        }

        unsafe {
            let lib = Library::new(lib)?;
            Ok(ViewLib {
                view_process_main_fn: *match lib.get(b"extern_view_process_main") {
                    Ok(f) => f,
                    // try old name (<=0.6.2)
                    Err(e) => match lib.get(b"extern_init") {
                        Ok(f) => f,
                        Err(_) => return Err(e.into()),
                    },
                },
                run_same_process_fn: *lib.get(b"extern_run_same_process")?,
                _lib: lib,
            })
        }
    }

    /// Call the pre-built [`view_process_main`].
    ///
    /// # Aborts
    ///
    /// Kills the process with code `101` if there is a panic generated by the pre-built code or by threads started by the pre-build code,
    /// this needs to happen because unwind across FFI in undefined behavior.
    ///
    /// [`view_process_main`]: https://docs.rs/zng-view/fn.view_process_main.html
    pub fn view_process_main(self) {
        unsafe { (self.view_process_main_fn)() }
    }

    /// Call the pre-build [`run_same_process`].
    ///
    /// This function exits the process after `run_app` returns.
    ///
    /// # Aborts
    ///
    /// Kills the process with code `101` if there is a panic generated by the pre-built code or by threads started by the pre-build code,
    /// this needs to happen because unwind across FFI in undefined behavior.
    ///
    /// [`run_same_process`]: https://docs.rs/zng-view/fn.run_same_process.html
    pub fn run_same_process(self, run_app: impl FnOnce() + Send + 'static) -> ! {
        self.run_same_process_impl(Box::new(run_app))
    }
    fn run_same_process_impl(self, run_app: Box<dyn FnOnce() + Send>) -> ! {
        let patch = StaticPatch::capture();
        unsafe {
            use std::sync::atomic::{AtomicU8, Ordering};

            static STATE: AtomicU8 = AtomicU8::new(ST_NONE);
            const ST_NONE: u8 = 0;
            const ST_SOME: u8 = 1;
            const ST_TAKEN: u8 = 2;

            static mut RUN: Option<Box<dyn FnOnce() + Send>> = None;

            if STATE.swap(ST_SOME, Ordering::SeqCst) != ST_NONE {
                panic!("expected only one call to `run_same_process`");
            }

            RUN = Some(run_app);

            extern "C" fn run() {
                if STATE.swap(ST_TAKEN, Ordering::SeqCst) != ST_SOME {
                    panic!("expected only one call to `run_app` closure");
                }

                let run_app = unsafe { RUN.take() }.unwrap();
                run_app();
            }

            (self.run_same_process_fn)(&patch, run)
        }
        // exit the process to ensure all threads are stopped
        zng_env::exit(0)
    }
}

#[cfg(zng_lib_embedded)]
const LIB: &[u8] = include_bytes!(env!("ZNG_VIEW_LIB"));
#[cfg(zng_lib_embedded)]
const LIB_NAME: &str = concat!("zv.", env!("CARGO_PKG_VERSION"), ".", env!("ZNG_VIEW_LIB_HASH"));

/// Error searching or linking to pre-build library.
#[derive(Debug)]
pub enum Error {
    /// Error searching library.
    Io(io::Error),
    /// Error loading or linking library.
    Lib(libloading::Error),
}
impl From<io::Error> for Error {
    fn from(e: io::Error) -> Self {
        Error::Io(e)
    }
}
impl From<libloading::Error> for Error {
    fn from(e: libloading::Error) -> Self {
        Error::Lib(e)
    }
}
impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Error::Io(e) => write!(f, "{e}"),
            Error::Lib(e) => write!(f, "{e}"),
        }
    }
}
impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Error::Io(e) => Some(e),
            Error::Lib(e) => Some(e),
        }
    }
}