dlrkit 0.1.3

dlrkit provides an API for interacting with the dynamic linker and loading libraries at runtime such as .so files and .dll files. Useful for executing code from external libraries using the C ABI.
Documentation
use core::ffi::{c_int, c_void};

use std::{
    error::Error,
    marker::PhantomData,
    ops::{Deref, DerefMut},
    path::Path,
};

#[cfg(unix)]
pub mod unix;

#[cfg(windows)]
pub mod windows;

/// OpenMode specifies the options for the Dl::open_mode function.
pub enum OpenMode {
    Unix(c_int),
    Win32(u32),
}

impl std::fmt::Display for OpenMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            OpenMode::Unix(_) => write!(f, "unix")?,
            OpenMode::Win32(_) => write!(f, "windows")?,
        }

        Ok(())
    }
}

/// Dl represents a dynamic linker object such as a library.
///
/// Libraries can be loaded using the Dl::open and Dl::open_mode
/// associated functions.
pub struct Dl {
    hnd: *mut c_void,
}

unsafe impl Send for Dl {}
unsafe impl Sync for Dl {}

impl Dl {
    /// open loads a library such as a .so file on Unix-like systems or
    /// a .dll on Windows. It is a wrapper for the open_mode function,
    /// refer to that function's documentation for more details.
    ///
    /// # Safety
    ///
    /// This function is unsafe because it relies on OS APIs that
    /// provide no memory safety assurances.
    ///
    /// # Examples
    ///
    /// Load a shared library and write the library handle's
    /// address to stderr:
    ///
    /// ```no_run
    /// const LIBRARY_PATH: &str = {
    ///     if cfg!(target_os = "windows") {
    ///         "target\\debug\\deps\\example_lib.dll"
    ///     } else if cfg!(any(target_os = "macos", target_os = "ios")) {
    ///         "target/debug/deps/libexample_lib.dylib"
    ///     } else {
    ///         "target/debug/deps/libexample_lib.so"
    ///     }
    /// };
    ///
    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let lib = unsafe { dlrkit::Dl::open(Some(LIBRARY_PATH))? };
    ///
    ///     eprintln!("library pointer: {:#x?}", unsafe { lib.handle() });
    ///
    ///     Ok(())
    /// }
    /// ```
    pub unsafe fn open<P: AsRef<Path>>(file: Option<P>) -> Result<Self, Box<dyn Error>> {
        #[cfg(unix)]
        unsafe {
            Dl::open_mode(file, OpenMode::Unix(unix::RTLD_NOW))
        }

        #[cfg(windows)]
        unsafe {
            Dl::open_mode(file, OpenMode::Win32(0))
        }
    }

    /// open_mode loads a library such as a .so file on Unix-like systems
    /// or a .dll on Windows according to the options specified by the
    /// OpenMode type. Users should generally use the Dl::open function
    /// instead. The purpose of open_mode is to expose platform-specific
    /// dynamic linker configuration.
    ///
    /// # Safety
    ///
    /// This function is unsafe because it relies on OS APIs that
    /// provide no memory safety assurances.
    ///
    /// # Arguments
    ///
    /// * file - The path to the dynamic library to load. If set to
    ///   None a pointer to the process' executable will be returned.
    /// * mode - Configures the behavior of the dynamic linker when
    ///   loading the library.
    pub unsafe fn open_mode<P: AsRef<Path>>(
        file: Option<P>,
        mode: OpenMode,
    ) -> Result<Self, Box<dyn Error>> {
        let result;

        #[cfg(unix)]
        unsafe {
            result = unix::do_dlopen(file, mode);
        }

        #[cfg(windows)]
        unsafe {
            result = windows::load_library_or_get_module_handle(file, mode)
        }

        Ok(Self { hnd: result? })
    }

    /// handle returns the underlying pointer to the memory-mapped object.
    pub unsafe fn handle(&self) -> *mut c_void {
        self.hnd
    }

    /// sym looks for a symbol in the current object by its name.
    ///
    /// # Safety
    ///
    /// This function is unsafe because it relies on OS APIs that
    /// provide no memory safety assurances.
    ///
    /// # Arguments
    ///
    /// * `symbol_name` - The name of the symbol to lookup.
    ///
    /// # Examples
    ///
    /// Load a shared library, create a function pointer for
    /// the library's "add" function, and execute the add
    /// function:
    ///
    /// ```no_run
    /// const LIBRARY_PATH: &str = {
    ///     if cfg!(target_os = "windows") {
    ///         "target\\debug\\deps\\example_lib.dll"
    ///     } else if cfg!(any(target_os = "macos", target_os = "ios")) {
    ///         "target/debug/deps/libexample_lib.dylib"
    ///     } else {
    ///         "target/debug/deps/libexample_lib.so"
    ///     }
    /// };
    ///
    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let lib = unsafe { dlrkit::Dl::open(Some(LIBRARY_PATH))? };
    ///
    ///     let add = unsafe { lib.sym::<fn(left: u64, right: u64) -> u64>("add")? };
    ///
    ///     // Outputs:
    ///     // add result: 0x41
    ///     eprintln!("add result: {}", add(0x40, 1));
    ///
    ///     Ok(())
    /// }
    /// ```
    pub unsafe fn sym<T>(&self, symbol_name: &str) -> Result<Sym<'_, T>, Box<dyn Error>> {
        // This check comes from dlopen2. It ensures that T is
        // the same size as a pointer.
        //
        // Copyright (c) 2017 Szymon Wieloch
        // Copyright (C) 2019 Ahmed Masud <ahmed.masud@saf.ai>
        // Copyright (C) 2022 OpenByte <development.openbyte@gmail.com>
        if size_of::<T>() != size_of::<*mut ()>() {
            panic!("type T has a different size than a pointer");
        }

        let result;

        #[cfg(unix)]
        unsafe {
            result = unix::do_dlsym(self.hnd, symbol_name);
        }

        #[cfg(windows)]
        unsafe {
            result = windows::get_proc_address(self.hnd, symbol_name);
        }

        let sym_ptr = result?;

        // Based on work by Chayim Friedman:
        // https://stackoverflow.com/a/71373744
        let sym_transmute = unsafe { std::mem::transmute_copy(&sym_ptr) };

        Ok(Sym::new(sym_transmute, sym_ptr as usize))
    }

    /// close unloads the underlying memory-mapped object.
    ///
    /// # Safety
    ///
    /// This function is unsafe because it relies on OS APIs that
    /// provide no memory safety assurances.
    pub unsafe fn close(self) -> Result<(), Box<dyn Error>> {
        #[cfg(unix)]
        unsafe {
            unix::do_dlclose(self.hnd)
        }

        #[cfg(windows)]
        unsafe {
            windows::free_library(self.hnd)
        }
    }
}

/// Sym is a safe wrapper around a symbol obtained from `Dl`.
/// Users should use the Dl::sym method instead of invoking
/// this type's "new" associated function directly.
///
/// This is the most generic type, valid for obtaining functions,
/// references and pointers. It does not accept null value of
/// the library symbol. Other types may provide more specialized
/// functionality better for some use cases.
///
/// This code originally appeared in the dlopen2 Rust library,
/// maintained by OpenByteDev.
///
/// * Copyright (c) 2017 Szymon Wieloch
/// * Copyright (C) 2019 Ahmed Masud <ahmed.masud@saf.ai>
/// * Copyright (C) 2022 OpenByte <development.openbyte@gmail.com>
#[derive(Debug, Clone, Copy)]
pub struct Sym<'lib, T: 'lib> {
    symbol: T,
    addr: usize,
    pd: PhantomData<&'lib T>,
}

impl<'lib, T> Sym<'lib, T> {
    pub fn new(symbol: T, addr: usize) -> Sym<'lib, T> {
        Sym {
            symbol,
            addr,
            pd: PhantomData,
        }
    }

    pub fn addr(&self) -> usize {
        self.addr
    }
}

impl<'lib, T> Deref for Sym<'lib, T> {
    type Target = T;
    fn deref(&self) -> &T {
        &self.symbol
    }
}

impl<'lib, T> DerefMut for Sym<'lib, T> {
    fn deref_mut(&mut self) -> &mut T {
        &mut self.symbol
    }
}

unsafe impl<'lib, T: Send> Send for Sym<'lib, T> {}
unsafe impl<'lib, T: Sync> Sync for Sym<'lib, T> {}