hexchat_api/
hook.rs

1
2//! This object wraps hook pointers returned when callbacks are registered
3//! with Hexchat. The Rust-style hook can be used to unhook commands directly
4//! via its `unhook()` function. This object protects against attempts to
5//! unhook the same callback more than once, which can crash Hexchat.
6//!
7//! The `unhook()` function returns the user_data that was registered with
8//! the associated callback, passing ownership to the caller. Invoking
9//! `unhook()` more than once returns `None`.
10//!
11//! The hooks can be cloned. Internally, clones safely share the same hook
12//! pointer. When hooks go out of scope, they do not remove their associated
13//! commands. Hooks can be ignored by the plugin if there is no need to
14//! unhook commands. The most relevant use of a hook could be to cancel
15//! timer callbacks.
16
17use libc::c_void;
18use std::ptr::null;
19use std::sync::RwLock;
20use std::sync::Arc;
21
22use send_wrapper::SendWrapper;
23
24use crate::callback_data::*;
25use crate::hexchat_entry_points::PHEXCHAT;
26use crate::user_data::*;
27
28/// A synchronized global list of the hooks. This gets initialized when a
29/// plugin is loaded from within the `lib_hexchat_plugin_init()` function
30/// before the plugin author's registered init function is invoked.
31///
32static HOOK_LIST: RwLock<Option<Vec<Hook>>> = RwLock::new(None);
33
34use UserData::*;
35
36struct HookData {
37    hook_ptr    : *const c_void,
38    cbd_box_ptr : *const c_void,
39}
40
41/// A wrapper for Hexchat callback hooks. These hooks are returned when
42/// registering callbacks and can be used to unregister (unhook) them.
43/// `Hook`s can be cloned to share a reference to the same callback hook.
44///
45#[derive(Clone)]
46pub struct Hook {
47    data: Arc<RwLock<Option<SendWrapper<HookData>>>>,
48}
49
50unsafe impl Send for Hook {}
51
52impl Hook {
53    /// Constructor. `hook` is a hook returned by Hexchat when registering a
54    /// C-facing callback.
55    ///
56    pub (crate) fn new() -> Self {
57
58        let hook = Hook {
59            data: Arc::new(
60                    RwLock::new(
61                        Some(
62                            SendWrapper::new(
63                                HookData {
64                                    hook_ptr    : null::<c_void>(),
65                                    cbd_box_ptr : null::<c_void>(),
66                        })))),
67        };
68
69        if let Some(hook_list) = HOOK_LIST.write().unwrap().as_mut() {
70            // Clean up dead hooks.
71            hook_list.retain(|h|
72                !h.data.read().unwrap().as_ref().unwrap().hook_ptr.is_null()
73            );
74
75            // Store newly created hook in global list.
76            hook_list.push(hook.clone());
77        }
78
79        hook
80    }
81
82    /// Sets the value of the internal hook pointer. This is used by the hooking
83    /// functions in hexchat.rs.
84    ///
85    pub (crate) fn set(&self, ptr: *const c_void) {
86        if let Some(_hook_list_locked) = HOOK_LIST.read().unwrap().as_ref() {
87            // Lock the global list, and set the internal pointer.
88            self.data.write().unwrap().as_mut().unwrap().hook_ptr = ptr;
89        }
90    }
91
92    /// Sets the Hook's internal pointer to the raw Box pointer that references
93    /// the `CallbackData`. We have to keep our own reference to any `user_data`
94    /// passed to Hexchat, because it doesn't seem to be playing nice during
95    /// unload, where it should be returning our user data on `unhook()` -
96    /// but it doesn't seem to be doing that.
97    ///
98    pub (crate) fn set_cbd(&self, ptr: *const c_void) {
99        if let Some(_hook_list_locked) = HOOK_LIST.read().unwrap().as_ref() {
100            // Lock the global list, and set the internal pointer.
101            self.data.write().unwrap().as_mut().unwrap().cbd_box_ptr = ptr;
102        }
103    }
104
105    /// Unhooks the related callback from Hexchat. The user_data object is
106    /// returned. Subsequent calls to `unhook()` will return `None`. The
107    /// callback that was registered with Hexchat will be unhooked and dropped.
108    /// Ownership of the `user_data` will be passed to the caller.
109    /// # Returns
110    /// * The user data that was registered with the callback using one of the
111    ///   hexchat hook functions.
112    ///
113    pub fn unhook(&self) -> UserData {
114        unsafe {
115            if let Some(_hook_list) = HOOK_LIST.read().unwrap().as_ref() {
116                let ptr_data = &mut self.data.write().unwrap();
117
118                // Determine if the Hook is still alive (non-null ptr).
119                if !ptr_data.as_ref().unwrap().hook_ptr.is_null() {
120
121                    // Unhook the callback.
122                    let hc = &*PHEXCHAT;
123                    let hp = ptr_data.as_ref().unwrap().hook_ptr;
124                    let _  = (hc.c_unhook)(hc, hp);
125
126                    // ^ _ should be our user_data, but we can't rely on Hexchat
127                    // to return a valid user_data pointer on unload, so we have
128                    // to maintain it ourselves.
129
130                    // Null the hook pointer.
131                    ptr_data.as_mut().unwrap().hook_ptr = null::<c_void>();
132
133                    // Reconstitute the CallbackData Box.
134                    let cd = ptr_data.as_ref().unwrap().cbd_box_ptr;
135                    let cd = &mut (*(cd as *mut CallbackData));
136                    let cd = &mut Box::from_raw(cd);
137
138                    // Give the caller the `user_data` the plugin registered
139                    // with the callback.
140                    return cd.take_data();
141                }
142            }
143            NoData
144        }
145    }
146
147    /// Called automatically within `lib_hexchat_plugin_init()` when a plugin is
148    /// loaded. This initializes the synchronized global static hook list.
149    ///
150    pub (crate) fn init() {
151        if let Ok(mut wlock) = HOOK_LIST.write() {
152            *wlock = Some(Vec::new());
153        }
154    }
155
156    /// Called when a plugin is unloaded by Hexchat. This happens when the user
157    /// opens the "Plugins and Scripts" dialog and unloads/reloads the plugin,
158    /// or the user issues one of the slash "/" commands to perform the same
159    /// operation. This function iterates over each hook, calling their
160    /// `unhook()` method which grabs ownership of the `CallbackData` objects
161    /// and drops them as they go out of scope ensuring their destructors
162    /// are called.
163    ///
164    pub (crate) fn deinit() {
165        if let Some(hook_list) = HOOK_LIST.read().unwrap().as_ref() {
166            for hook in hook_list {
167                hook.unhook();
168            }
169        }
170        // This causes the `RwLock` and hook vector to be dropped.
171        // plugin authors need to ensure that no threads are running when
172        // their plugins are unloading - or one may try to access the lock
173        // and hook vector after they've been destroyed.
174        if let Ok(mut hook_list_opt) = HOOK_LIST.write() {
175            hook_list_opt.take();
176        }
177    }
178}
179
180
181