hotload 1.2.0

Zero cost hot update dynamic library; supporting DLL, SO
Documentation
use std::cell::Cell;
use std::{ops::Deref, path::Path, sync::Arc};

use dlopen2::wrapper::{Container, WrapperApi};
use notify::{Event, RecursiveMode, Watcher};

use crate::{
    R,
    event::{EventNewDll, EventOldDll},
};

pub const MAX_LOAD_INDEX: usize = 10;

pub struct HotloadInner<API: WrapperApi> {
    pub path: String,
    pub container: [Option<Container<API>>; MAX_LOAD_INDEX],
    pub load_index: usize, // default container.len, current container not loaded
    pub watch: Option<notify::RecommendedWatcher>,
}

impl<API: WrapperApi> HotloadInner<API> {
    pub fn new(path: impl Into<String>) -> Self {
        Self {
            path: path.into(),
            container: [None, None, None, None, None, None, None, None, None, None],
            load_index: MAX_LOAD_INDEX,
            watch: None,
        }
    }
}

pub struct Hotload<API: WrapperApi> {
    pub(crate) inner: UA<HotloadInner<API>>,
    file_path: &'static str,
}

impl<API: WrapperApi + 'static> Deref for Hotload<API> {
    type Target = Container<API>;
    fn deref(&self) -> &Self::Target {
        let inner = &*self.inner;
        let inner = &inner.container[inner.load_index];
        inner.as_ref().expect("container not loaded")
    }
}

const HOTLOAD_CACHE_DIR: &str = ".hotload_cache_dir";

impl<API: WrapperApi + 'static + Send> Hotload<API> {
    pub const fn new(dll_path: &'static str) -> Self {
        Self {
            inner: UA::new(),
            file_path: dll_path,
        }
    }

    fn get_dir(path: &str) -> String {
        let dir = {
            let path = path;
            let p = Path::new(&path);
            let mut p = p.components().collect::<Vec<_>>();
            p.pop();
            p.iter()
                .map(|x| x.as_os_str().to_str().unwrap_or_else(|| ""))
                .collect::<Vec<_>>()
                .join("/")
        };
        dir
    }

    // load new dll is name
    pub fn dll_name(&self) -> String {
        get_file_name(self.file_path)
            .unwrap_or_else(|_| "".to_string())
            .to_string()
    }

    /// "dll created" or "file rename to" is actively loaded
    /// frist time load EventOldDll is None
    pub fn init_load<F: FnMut(EventNewDll<API>, Option<EventOldDll<API>>) + Send + 'static>(
        &self,
        mut event_call: F,
    ) -> R<()> {
        // 之前只是初始化, 现在才是真的加载
        self.inner.set(HotloadInner::new(self.file_path));
        let dir = Self::get_dir(self.file_path);
        let file_name = get_file_name(&self.file_path)?;

        let dir_cahe = Path::new(&dir);
        let mut dir_cahe = dir_cahe.to_path_buf();
        dir_cahe.push(HOTLOAD_CACHE_DIR);
        _ = std::fs::create_dir(dir_cahe.clone());

        let new_event = EventNewDll::new(self.inner.clone(), dir_cahe.clone())?;
        event_call(new_event, None);

        let inner_c = self.inner.clone();

        let dir_cahe_c = dir_cahe.clone();
        let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
            let event = match res {
                Ok(v) => v,
                Err(e) => {
                    error!("watch error: {e:?}");
                    return;
                }
            };

            // The file in the incident is not a monitored file and will not be processed
            if event
                .paths
                .iter()
                .find(|x| {
                    x.file_name()
                        .map(|x| x.to_str().unwrap_or_else(|| "") == file_name)
                        .unwrap_or_else(|| false)
                })
                .is_none()
            {
                return;
            }

            let mut is_action = false;
            match &event.kind {
                notify::EventKind::Create(_e) => is_action = true,
                notify::EventKind::Modify(notify::event::ModifyKind::Name(
                    notify::event::RenameMode::To,
                )) => is_action = true,
                _ => (),
            };

            debug!("event: {:?}; Is action load dll: {is_action}", event);

            if is_action {
                let load_new = match EventNewDll::new(inner_c.clone(), dir_cahe_c.clone()) {
                    Ok(v) => v,
                    Err(e) => {
                        error!("load new error: {e:?}");
                        return;
                    }
                };
                let release_old = EventOldDll::new(inner_c.clone());
                event_call(load_new, Some(release_old));
            }
        })?;

        let inner = self.inner.borrow_mut();

        // watch directory
        debug!("hoload watch path: {dir:?}");
        watcher.watch(Path::new(&dir), RecursiveMode::NonRecursive)?;
        inner.watch = Some(watcher);

        Ok(())
    }

    pub fn borrow_mut(&self) -> &mut Container<API> {
        let inner = self.inner.borrow_mut();
        let inner = &mut inner.container[inner.load_index];
        inner.as_mut().expect("container not loaded")
    }
}

impl<API: WrapperApi> Hotload<API> {
    /// 强行卸载
    pub fn force_release(&self) {
        let inner = self.inner.borrow_mut();
        if inner.load_index == MAX_LOAD_INDEX {
            return;
        }
        for ele in inner.container.iter_mut() {
            *ele = None;
        }
        inner.load_index = MAX_LOAD_INDEX;
    }
}

impl<API: WrapperApi> Drop for Hotload<API> {
    fn drop(&mut self) {
        self.force_release();
    }
}

/// 轻量级共享可变容器。
/// 内部使用 `Cell<*mut T>` + `Arc` 原始指针 API 管理生命周期,
/// 无 RefCell 运行时开销,无原子操作开销,无嵌套。
pub struct UA<T> {
    /// null = 未初始化; 非null = 指向 Arc 分配的 T
    ptr: Cell<*mut T>,
}

impl<T> Deref for UA<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        self.borrow()
    }
}

impl<T> Clone for UA<T> {
    fn clone(&self) -> Self {
        let ptr = self.ptr.get();
        if !ptr.is_null() {
            // 增加 Arc 引用计数,确保克隆共享同一份数据
            unsafe {
                Arc::increment_strong_count(ptr as *const T);
            }
        }
        Self {
            ptr: Cell::new(ptr),
        }
    }
}

impl<T> UA<T> {
    pub const fn new() -> Self {
        Self {
            ptr: Cell::new(std::ptr::null_mut()),
        }
    }

    pub fn set(&self, u: T) {
        let new_ptr = Arc::into_raw(Arc::new(u)) as *mut T;
        let old_ptr = self.ptr.replace(new_ptr);
        if !old_ptr.is_null() {
            // 旧值引用计数 -1,若为最后一个引用则释放
            unsafe {
                drop(Arc::from_raw(old_ptr as *const T));
            }
        }
    }

    pub fn borrow_mut(&self) -> &mut T {
        let ptr = self.ptr.get();
        assert!(!ptr.is_null(), "hotload dll value is not init");
        unsafe { &mut *ptr }
    }

    pub fn borrow(&self) -> &T {
        let ptr = self.ptr.get();
        assert!(!ptr.is_null(), "hotload dll value is not init");
        unsafe { &*ptr }
    }
}

impl<T> Drop for UA<T> {
    fn drop(&mut self) {
        let ptr = self.ptr.get();
        if !ptr.is_null() {
            unsafe {
                drop(Arc::from_raw(ptr as *const T));
            }
        }
    }
}

unsafe impl<T> Sync for UA<T> {}
unsafe impl<T> Send for UA<T> {}

#[test]
fn test_ua() {
    unsafe {
        std::env::set_var("RUST_LOG", "debug");
    }
    env_logger::init();

    // test value
    let a: UA<i32> = UA::new();
    a.set(1);
    assert_eq!(*a, 1);
    a.set(2);
    assert_eq!(*a, 2);
    assert_eq!(*a, 2);

    // test struct fn
    struct T {
        v: String,
    }
    impl T {
        fn test(&self) -> i32 {
            2
        }
    }
    let a: UA<T> = UA::new();
    a.set(T { v: "1".to_string() });
    assert_eq!(a.test(), 2);
    assert_eq!(a.test(), 2);

    // test error
    assert_eq!(a.v.as_str(), "1");

    // test clone
    let a = a.clone();
    assert_eq!(a.test(), 2);
    assert_eq!(a.v, "1");

    // two set
    a.set(T {
        v: "22".to_string(),
    });
    assert_eq!(a.test(), 2);
    assert_eq!(a.v, "22");
    assert_eq!(a.v, "22");
}

#[test]
fn test_ua_多线程() {
    unsafe { std::env::set_var("RUST_LOG", "debug") };
    env_logger::init();

    // test value
    let a: UA<i32> = UA::new();
    a.set(1);
    assert_eq!(*a, 1);
    a.set(2);
    assert_eq!(*a, 2);
    assert_eq!(*a, 2);

    // test struct fn
    struct T {
        v: String,
    }
    impl T {
        fn test(&self) -> i32 {
            self.v.parse().unwrap_or_else(|_| -1)
        }
    }

    static A: UA<T> = UA::new();
    A.set(T { v: "1".to_string() });

    for _ in 0..5 {
        std::thread::spawn(move || {
            loop {
                assert_eq!(A.test(), 1);
            }
        });
    }

    for _ in 0..5 {
        std::thread::spawn(move || {
            loop {
                assert_eq!(A.test(), 1);
            }
        });
    }

    std::thread::sleep(std::time::Duration::from_secs(9999999999));
}

pub(crate) fn get_file_name<S: AsRef<std::ffi::OsStr> + ?Sized>(s: &S) -> R<String> {
    // debug!("get_file_name: {:?}", s.as_ref());
    let f_old: &Path = std::path::Path::new(s);
    if !std::fs::exists(f_old)? {
        return Err(format!("get_file_name not exists: {:?}", s.as_ref()))?;
    }
    let f_name = f_old
        .file_name()
        .ok_or_else(|| "file name not find")?
        .to_str()
        .ok_or_else(|| "file name not find")?;
    Ok(f_name.to_string())
}