1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191

//! This object wraps hook pointers returned when callbacks are registered
//! with Hexchat. The Rust-style hook can be used to unhook commands directly
//! via its `unhook()` function. This object protects against attempts to
//! unhook the same callback more than once, which can crash Hexchat.
//!
//! The `unhook()` function returns the user_data that was registered with
//! the associated callback, passing ownership to the caller. Invoking
//! `unhook()` more than once returns `None`.
//!
//! The hooks can be cloned. Internally, clones safely share the same hook
//! pointer. When hooks go out of scope, they do not remove their associated
//! commands. Hooks can be ignored by the plugin if there is no need to
//! unhook commands. The most relevant use of a hook could be to cancel
//! timer callbacks.

use libc::c_void;
use std::ptr::null;
use std::sync::RwLock;
use std::sync::Arc;

use send_wrapper::SendWrapper;

use crate::callback_data::*;
use crate::hexchat_entry_points::PHEXCHAT;
use crate::user_data::*;

/// A synchronized global list of the hooks. This gets initialized when a
/// plugin is loaded from within the `lib_hexchat_plugin_init()` function
/// before the plugin author's registered init function is invoked.
///
static mut HOOK_LIST: Option<RwLock<Vec<Hook>>> = None;

use UserData::*;

struct HookData {
    hook_ptr    : *const c_void,
    cbd_box_ptr : *const c_void,
}

/// A wrapper for Hexchat callback hooks. These hooks are returned when
/// registering callbacks and can be used to unregister (unhook) them.
/// `Hook`s can be cloned to share a reference to the same callback hook.
///
#[derive(Clone)]
pub struct Hook {
    data: Arc<RwLock<Option<SendWrapper<HookData>>>>,
}

unsafe impl Send for Hook {}

impl Hook {
    /// Constructor. `hook` is a hook returned by Hexchat when registering a
    /// C-facing callback.
    ///
    pub (crate) fn new() -> Self {

        let hook = Hook {
            data: Arc::new(
                    RwLock::new(
                        Some(
                            SendWrapper::new(
                                HookData {
                                    hook_ptr    : null::<c_void>(),
                                    cbd_box_ptr : null::<c_void>(),
                        })))),
        };

        if let Some(hook_list_rwlock) = unsafe { &HOOK_LIST } {
            // Acquire global hook list write lock.
            let wlock     = hook_list_rwlock.write();
            let hook_list = &mut *wlock.unwrap();

            // Clean up dead hooks.
            hook_list.retain(|h|
                !h.data.read().unwrap().as_ref().unwrap().hook_ptr.is_null()
            );

            // Store newly created hook in global list.
            hook_list.push(hook.clone());
        }

        hook
    }

    /// Sets the value of the internal hook pointer. This is used by the hooking
    /// functions in hexchat.rs.
    ///
    pub (crate) fn set(&self, ptr: *const c_void) {
        if let Some(hl_rwlock) = unsafe { &HOOK_LIST } {
            // Lock the global list, and set the internal pointer.
            let _rlock = hl_rwlock.read();
            self.data.write().unwrap().as_mut().unwrap().hook_ptr = ptr;
        }
    }

    /// Sets the Hook's internal pointer to the raw Box pointer that references
    /// the `CallbackData`. We have to keep our own reference to any `user_data`
    /// passed to Hexchat, because it doesn't seem to be playing nice during
    /// unload, where it should be returning our user data on `unhook()` -
    /// but it doesn't seem to be doing that.
    ///
    pub (crate) fn set_cbd(&self, ptr: *const c_void) {
        if let Some(hl_rwlock) = unsafe { &HOOK_LIST } {
            // Lock the global list, and set the internal pointer.
            let _rlock = hl_rwlock.read();
            self.data.write().unwrap().as_mut().unwrap().cbd_box_ptr = ptr;
        }
    }

    /// Unhooks the related callback from Hexchat. The user_data object is
    /// returned. Subsequent calls to `unhook()` will return `None`. The
    /// callback that was registered with Hexchat will be unhooked and dropped.
    /// Ownership of the `user_data` will be passed to the caller.
    /// # Returns
    /// * The user data that was registered with the callback using one of the
    ///   hexchat hook functions.
    ///
    pub fn unhook(&self) -> UserData {
        unsafe {
            if let Some(hl_rwlock) = &HOOK_LIST {
                let _rlock = hl_rwlock.read();

                let ptr_data = &mut self.data.write().unwrap();

                // Determine if the Hook is still alive (non-null ptr).
                if !ptr_data.as_ref().unwrap().hook_ptr.is_null() {

                    // Unhook the callback.
                    let hc = &*PHEXCHAT;
                    let hp = ptr_data.as_ref().unwrap().hook_ptr;
                    let _  = (hc.c_unhook)(hc, hp);

                    // ^ _ should be our user_data, but we can't rely on Hexchat
                    // to return a valid user_data pointer on unload, so we have
                    // to maintain it ourselves.

                    // Null the hook pointer.
                    ptr_data.as_mut().unwrap().hook_ptr = null::<c_void>();

                    // Reconstitute the CallbackData Box.
                    let cd = ptr_data.as_ref().unwrap().cbd_box_ptr;
                    let cd = &mut (*(cd as *mut CallbackData));
                    let cd = &mut Box::from_raw(cd);

                    // Give the caller the `user_data` the plugin registered
                    // with the callback.
                    return cd.take_data();
                }
            }
            NoData
        }
    }

    /// Called automatically within `lib_hexchat_plugin_init()` when a plugin is
    /// loaded. This initializes the synchronized global static hook list.
    ///
    pub (crate) fn init() {
        unsafe {
            HOOK_LIST = Some(RwLock::new(Vec::new()));
        }
    }

    /// Called when a plugin is unloaded by Hexchat. This happens when the user
    /// opens the "Plugins and Scripts" dialog and unloads/reloads the plugin,
    /// or the user issues one of the slash "/" commands to perform the same
    /// operation. This function iterates over each hook, calling their
    /// `unhook()` method which grabs ownership of the `CallbackData` objects
    /// and drops them as they go out of scope ensuring their destructors
    /// are called.
    ///
    pub (crate) fn deinit() {
        if let Some(hl_rwlock) = unsafe { &HOOK_LIST } {
            let rlock = hl_rwlock.read();
            let hook_list = &*rlock.unwrap();
            for hook in hook_list {
                hook.unhook();
            }
        }
        unsafe {
            // This causes the `RwLock` and hook vector to be dropped.
            // plugin authors need to ensure that no threads are running when
            // their plugins are unloading - or one may try to access the lock
            // and hook vector after they've been destroyed.
            let _ = HOOK_LIST.take();
        }
    }
}