rhai-dylib 0.10.0

Dylib support for Rhai
Documentation
//! # dylib loader.
//!
//! The [`Libloading`] loader enables you to expend functionality of a [`rhai::Engine`] via dynamic libraries using [`libloading`](https://github.com/nagisa/rust_libloading).
//!
//! You need to declare the entrypoint function of your module, following the [`Entrypoint`] prototype.
//! The name of the function must be the same as [`MODULE_ENTRYPOINT`].
//!
//! ```rust,ignore
//! fn module_entrypoint() -> rhai::Shared<rhai::Module> {
//!     // ...
//! }
//! ```
//!
//! You can easily, for example, implement and export your module using Rhai's [plugin modules](https://rhai.rs/book/plugins/module.html).
//!
//! ```rust,ignore
//! use rhai::plugin::*;
//!
//! // Use the `export_module` macro to generate your api.
//! #[export_module]
//! mod my_api {
//!     pub fn get_num() -> i64 {
//!         3
//!     }
//!     pub fn print_stuff() {
//!        println!("Hello World!");
//!     }
//! }
//!
//! // The entrypoint function of your module.
//! // `extern "C"` can be omitted if you are using the `rust` feature.
//! #[no_mangle]
//! extern "C" fn module_entrypoint() -> rhai::Shared<rhai::Module> {
//!     // Build your module.
//!     rhai::exported_module!(my_api).into()
//! }
//! ```

use super::Loader;

/// Entrypoint prototype for a Rhai module "constructor".
pub type Entrypoint = fn() -> rhai::Shared<rhai::Module>;
/// The name of the function that will be called to update the [`rhai::Engine`].
pub const MODULE_ENTRYPOINT: &str = "module_entrypoint";

/// Loading dynamic libraries using the [`libloading`](https://github.com/nagisa/rust_libloading) crate.
///
/// # Example
///
/// ```rust,ignore
/// // Create your dynamic library loader & rhai engine.
/// let mut loader = rhai_dylib::loader::libloading::Libloading::new();
/// let mut engine = rhai::Engine::new();
///
/// // `my_first_module` library exposes the `print_first` function.
/// loader.load("my_first_module.so", &mut engine).expect("failed to load library 1");
/// // `my_second_module` library exposes the `print_second` function.
/// loader.load("my_second_module.so", &mut engine).expect("failed to load library 2");
///
/// // functions are now registered in the engine and can be called !
/// engine.run(r"
///     print_first();
///     print_second();
/// ");
/// ```
pub struct Libloading {
    /// Libraries loaded in memory.
    libraries: Vec<libloading::Library>,
}

impl Default for Libloading {
    /// Create a new instance of the loader.
    fn default() -> Self {
        Self { libraries: vec![] }
    }
}

impl Libloading {
    /// Create a new instance of the loader.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }
}

impl Loader for Libloading {
    /// Load a rhai module from a dynamic library.
    fn load(
        &mut self,
        path: impl AsRef<std::path::Path>,
    ) -> Result<rhai::Shared<rhai::Module>, Box<rhai::EvalAltResult>> {
        let library = unsafe {
            #[cfg(target_os = "linux")]
            {
                // Workaround for a crash on library unloading on linux: https://github.com/nagisa/rust_libloading/issues/5#issuecomment-244195096
                libloading::os::unix::Library::open(
                    Some(path.as_ref()),
                    // Load library with `RTLD_NOW | RTLD_NODELETE` to fix SIGSEGV.
                    0x2 | 0x1000,
                )
                .map(libloading::Library::from)
            }

            #[cfg(any(target_os = "macos", target_os = "windows"))]
            {
                libloading::Library::new(path.as_ref())
            }
        }
        .map_err(|error| {
            rhai::EvalAltResult::ErrorInModule(
                path.as_ref()
                    .to_str()
                    .map_or(String::default(), std::string::ToString::to_string),
                error.to_string().into(),
                rhai::Position::NONE,
            )
        })?;

        self.libraries.push(library);
        let library = self.libraries.last().expect("library just got inserted");

        let module_entrypoint = unsafe { library.get::<Entrypoint>(MODULE_ENTRYPOINT.as_bytes()) }
            .map_err(|error| {
                rhai::EvalAltResult::ErrorInModule(
                    path.as_ref()
                        .to_str()
                        .map_or(String::default(), std::string::ToString::to_string),
                    error.to_string().into(),
                    rhai::Position::NONE,
                )
            })?;

        Ok(module_entrypoint())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::loader::Loader;

    fn build_test_plugin() -> &'static std::path::PathBuf {
        // Prevents multiple threads writing to the dll on windows and triggering a STATUS_ACCESS_VIOLATION error.
        static PATH: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
        PATH.get_or_init(|| {
            let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
            let status = std::process::Command::new("cargo")
                .args(["build", "--example", "test_plugin"])
                .current_dir(manifest_dir)
                .status()
                .expect("failed to execute cargo build");

            assert!(status.success(), "building test_plugin failed");

            let target_dir = std::env::var("CARGO_TARGET_DIR")
                .map(std::path::PathBuf::from)
                .unwrap_or_else(|_| manifest_dir.join("target"));

            #[cfg(target_os = "linux")]
            return target_dir.join("debug/examples/libtest_plugin.so");
            #[cfg(target_os = "macos")]
            return target_dir.join("debug/examples/libtest_plugin.dylib");
            #[cfg(target_os = "windows")]
            return target_dir.join("debug/examples/test_plugin.dll");
        })
    }

    #[test]
    fn new() {
        let _ = Libloading::new();
    }

    #[test]
    fn load_success() {
        let mut loader = Libloading::new();
        loader
            .load(build_test_plugin().as_path())
            .expect("failed to load test_plugin");
    }

    #[test]
    fn load_nonexistent_returns_error() {
        let mut loader = Libloading::new();
        let err = loader.load("nonexistent.so").unwrap_err();

        assert!(matches!(*err, rhai::EvalAltResult::ErrorInModule(..)));
    }
}