hexchat_api/
hexchat_entry_points.rs

1
2//! This module holds the DLL entry points for this plugin library.
3//! These exported functions are what Hexchat links to directly when the
4//! plugin is loaded.
5//!
6//! These functions register the plugin info with Hexchat and set up to "catch"
7//! any panics that Rust-side callbacks might "raise" during execution.
8//! Panics are displayed in the currently active Hexchat window/context.
9//! The debug build of the library will include a stack trace in the error
10//! message.
11
12#[cfg(debug_assertions)]
13use backtrace::Backtrace;
14use libc::c_char;
15use send_wrapper::SendWrapper;
16use std::ffi::CString;
17use std::marker::PhantomPinned;
18use std::panic;
19use std::panic::{catch_unwind, UnwindSafe};
20use std::pin::Pin;
21use std::ptr::null;
22use std::ptr::NonNull;
23use std::sync::RwLock;
24
25//use crate::{plugin_get_info, plugin_init, plugin_deinit};
26use crate::hexchat::Hexchat;
27use crate::hook::*;
28use crate::utils::*;
29#[cfg(feature = "threadsafe")]
30use crate::thread_facilities::*;
31
32/// The signature for the init function that plugin authors need to register
33/// using `dll_entry_points!()`.
34pub type InitFn   = dyn FnOnce(&'static Hexchat) -> i32 + UnwindSafe;
35
36/// The signature for the deinit function plugin authors need to register using
37/// `dll_entry_points!()`.
38pub type DeinitFn = dyn FnOnce(&'static Hexchat) -> i32 + UnwindSafe;
39
40/// The signature of the info function plugin authors need to register using
41/// `dll_entry_points!()`.
42pub type InfoFn   = dyn FnOnce() -> PluginInfo + UnwindSafe;
43
44/// Holds persistent client plugin info strings.
45static PLUGIN_INFO: RwLock<Option<SendWrapper<PluginInfo>>> = RwLock::new(None);
46
47/// The global Hexchat pointer obtained from `hexchat_plugin_init()`.
48pub(crate) static mut PHEXCHAT: *const Hexchat = null::<Hexchat>();
49
50/// `dll_entry_points()` makes it very easy to set up your plugin's DLL
51/// interface required by the Hexchat loader. This macro generates the necessary
52/// DLL entry points that Hexchat looks for when a DLL is being loaded.
53/// Normal Rust functions having the required signatures can be passed to the
54/// macro like so:
55///
56/// ```dll_entry_points!( my_info_func, my_init_func, my_deinit_func )```
57///
58/// That's it. You don't need to worry about how to export your Rust functions
59/// to interface with the C environment of Hexchat. This macro does it all for
60/// you.
61///
62/// # The signatures for the functions are:
63///
64/// * `my_info_func  ()                 -> PluginInfo;`
65/// * `my_init_func  (&'static Hexchat) -> i32;`
66/// * `my_deinit_func(&'static Hexchat) -> i32;`
67///
68/// The **info function** should create an instance of `PluginInfo` by calling
69/// its constructor with information about the plugin as parameters.
70///
71/// The **init function** is typically where you'll want to register your
72/// plugin's commands. Hook commands are provided by the `&Hexchat` reference
73/// provided as a paramter when your init function is called by Hexchat. The
74/// init function needs to return either 0 (good) or 1 (error).
75///
76/// The **deinit function** gets called when your plugin is unloaded. Return a
77/// 0 (good) or 1 (error). Any cleanup actions needed to be done can be done
78/// here. However, when your  DLL is unloaded by Hexchat, all its hooked
79/// commands are unhooked automatically - so you don't need to worry about
80/// managing the `Hook` objects returned by the hook commands unless you're
81/// plugin needs to for some reason. If your plugin creates any static
82/// variables, This is the place to drop their values, for example:
83/// `MY_STATIC_VAR = None;`
84///
85#[macro_export]
86macro_rules! dll_entry_points {
87
88    ( $info:ident, $init:ident, $deinit:ident ) => {
89        #[no_mangle]
90        pub extern "C"
91        fn hexchat_plugin_get_info(name     : *mut *const i8,
92                                   desc     : *mut *const i8,
93                                   version  : *mut *const i8,
94                                   reserved : *mut *const i8)
95        {
96            hexchat_api::lib_get_info(name,
97                                      desc,
98                                      version,
99                                      Box::new($info));
100        }
101        #[no_mangle]
102        pub extern "C"
103        fn hexchat_plugin_init(hexchat   : &'static Hexchat,
104                               name      : *mut *const i8,
105                               desc      : *mut *const i8,
106                               version   : *mut *const i8
107                              ) -> i32
108        {
109            hexchat_api::lib_hexchat_plugin_init(hexchat,
110                                                 name,
111                                                 desc,
112                                                 version,
113                                                 Box::new($init),
114                                                 Box::new($info))
115        }
116        #[no_mangle]
117        pub extern "C"
118        fn hexchat_plugin_deinit(hexchat : &'static Hexchat) -> i32
119        {
120            hexchat_api::lib_hexchat_plugin_deinit(hexchat, Box::new($deinit))
121        }
122    };
123}
124
125/// Holds client plugin information strings.
126struct PluginInfoData {
127    name         : CString,
128    version      : CString,
129    description  : CString,
130    pname        : NonNull<CString>,
131    pversion     : NonNull<CString>,
132    pdescription : NonNull<CString>,
133    _pin         : PhantomPinned,
134}
135
136/// Hexchat addons need to return an instance of this struct from their
137/// `plugin_info()` function, which gets called when Hexchat loads the addons.
138/// The `PluginInfo` object holds pinned internal buffers that Hexchat can
139/// read from at its leisure.
140///
141pub struct PluginInfo {
142    data: Pin<Box<PluginInfoData>>,
143}
144
145impl PluginInfo {
146    /// Constructor. The plugin information provided in the parameters is used
147    /// to create persistent pinned buffers that are guaranteed to be valid
148    /// for Hexchat to read from while the plugin is loading.
149    ///
150    /// # Arguments
151    /// * `name`        - The name of the plugin.
152    /// * `version`     - The plugin's version number.
153    /// * `description` - The plugin's description.
154    ///
155    /// # Returns
156    /// A `PluginInfo` object initialized from the parameter data.
157    ///
158    pub fn new(name: &str, version: &str, description: &str) -> PluginInfo
159    {
160        let pi = PluginInfoData {
161            name         : str2cstring(name),
162            version      : str2cstring(version),
163            description  : str2cstring(description),
164            pname        : NonNull::dangling(),
165            pversion     : NonNull::dangling(),
166            pdescription : NonNull::dangling(),
167            _pin         : PhantomPinned,
168        };
169        let mut boxed    = Box::pin(pi);
170        let sname        = NonNull::from(&boxed.name);
171        let sversion     = NonNull::from(&boxed.version);
172        let sdescription = NonNull::from(&boxed.description);
173
174        unsafe {
175            let mut_ref: Pin<&mut PluginInfoData> = Pin::as_mut(&mut boxed);
176            let unchecked = Pin::get_unchecked_mut(mut_ref);
177            unchecked.pname        = sname;
178            unchecked.pversion     = sversion;
179            unchecked.pdescription = sdescription;
180        }
181        PluginInfo { data: boxed }
182    }
183}
184
185/// Called indirectly when a plugin is loaded to get info about it. The
186/// plugin author shouldn't invoke this fuction - it's only public because
187/// the `dll_entry_points()` macro generates code that calls this.
188/// This function calls the client plugin's `plugin_get_info()` indirectly to
189/// obtain the persistent plugin info strings that it sets the paramters to.
190///
191#[doc(hidden)]
192pub fn lib_hexchat_plugin_get_info(name      : *mut *const i8,
193                                   desc      : *mut *const i8,
194                                   version   : *mut *const i8,
195                                   _reserved : *mut *const i8,
196                                   callback  : Box<InfoFn>)
197{
198    lib_get_info(name, desc, version, callback);
199}
200
201/// Called indirectly while a plugin is being loaded. The
202/// plugin author shouldn't invoke this fuction - it's only public because
203/// the `dll_entry_points()` macro generates code that calls this.
204///
205#[doc(hidden)]
206pub fn lib_hexchat_plugin_init(hexchat   : &'static Hexchat,
207                               name      : *mut *const c_char,
208                               desc      : *mut *const c_char,
209                               version   : *mut *const c_char,
210                               init_cb   : Box<InitFn>,
211                               info_cb   : Box<InfoFn>)
212    -> i32
213{
214    // Store the global Hexchat pointer.
215    unsafe { PHEXCHAT = hexchat; }
216
217    set_panic_hook(hexchat);
218
219    lib_get_info(name, desc, version, info_cb);
220
221    // Invoke client lib's init function.
222    catch_unwind(|| {
223        Hook::init();
224        #[cfg(feature = "threadsafe")]
225        main_thread_init();
226        init_cb(hexchat)
227    }).unwrap_or(0)
228}
229
230/// Invoked indirectly while a plugin is being unloaded. This function will
231/// call the deinitialization function that was registered using the
232/// `dll_entry_points()` macro. It will also unhook all the callbacks
233/// currently registered forcing them, and their closure state, to drop and
234/// thus clean up. Plugin authors should not call this - it's only public
235/// because `dll_entry_points()` generates code that needs this.
236///
237#[doc(hidden)]
238pub fn lib_hexchat_plugin_deinit(hexchat  : &'static Hexchat,
239                                 callback : Box<DeinitFn>)
240    -> i32
241{
242    let result = catch_unwind(|| {
243        // This has to be called before the user's deinit function to avoid the
244        // situation where their function tries to do a .join() on their threads
245        // which will block indefinitely waiting on AsyncResult.get().
246        #[cfg(feature = "threadsafe")]
247        main_thread_deinit();
248
249        // Call user's deinit().
250        let retval = callback(hexchat);
251
252        // Cause the callback_data objects to drop and clean up.
253        Hook::deinit();
254
255        // Destruct the info struct.
256        PLUGIN_INFO.write().unwrap().take();
257
258        retval
259    }).unwrap_or(0);
260    // Final clean up on unload - drop the hook closure.
261    let _ = panic::take_hook();
262    result
263}
264
265
266/// This function sets Hexchat's character pointer pointer's to point at the
267/// pinned buffers holding info about a plugin. Not to be called by plugin
268/// authors - it's only public because `dll_entry_points()` generates code
269/// that calls this.
270///
271#[inline]
272#[doc(hidden)]
273#[allow(clippy::not_unsafe_ptr_arg_deref)]
274pub fn lib_get_info(name     : *mut *const c_char,
275                    desc     : *mut *const c_char,
276                    vers     : *mut *const c_char,
277                    callback : Box<InfoFn>)
278{
279    unsafe {
280        if PLUGIN_INFO.read().unwrap().is_none() {
281            let pi = callback();
282            *PLUGIN_INFO.write().unwrap() = Some(SendWrapper::new(pi));
283        }
284        if let Some(info) = PLUGIN_INFO.read().unwrap().as_ref() {
285            *name = info.data.pname.as_ref().as_ptr();
286            *vers = info.data.pversion.as_ref().as_ptr();
287            *desc = info.data.description.as_ref().as_ptr();
288        }
289    }
290}
291
292/// Sets the panic hook so panic info will be printed to the active Hexchat
293/// window. The debug build includes a stack trace using
294/// [Backtrace](https://crates.io/crates/backtrace)
295fn set_panic_hook(hexchat: &'static Hexchat) {
296    panic::set_hook(Box::new(move |panic_info| {
297        #[cfg(debug_assertions)]
298        let mut loc = String::new();
299        let plugin_name;
300        if let Some(pi) = PLUGIN_INFO.read().unwrap().as_ref() {
301            plugin_name = pi.data.name.to_str().unwrap().to_string();
302        } else {
303            plugin_name = "a Rust plugin".to_string();
304        }
305        if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
306            hexchat.print(&format!("\x0304<<Panicked!>>\t{:?}", s));
307        } else {
308            hexchat.print(&format!("\x0304<<Panicked!>>\t{:?}", panic_info));
309        }
310        if let Some(location) = panic_info.location() {
311            hexchat.print(
312                &format!("\x0313Panic occured in {} in file '{}' at line {:?}.",
313                         plugin_name,
314                         location.file(),
315                         location.line()));
316
317            #[cfg(debug_assertions)]
318            { loc = format!("{}:{:?}", location.file(), location.line()); }
319        }
320        // For the debug build, include a stack trace.
321        #[cfg(debug_assertions)]
322        {
323            let mut trace = vec![];
324            let mut begin = 0;
325            let mut end   = 0;
326            let     bt    = Backtrace::new();
327            let     btstr = format!("{:?}", bt);
328
329            for line in btstr.lines() {
330                let line  = String::from(line);
331                if begin == 0 && !loc.is_empty() && line.contains(&loc) {
332                    // Underlined and magenta.
333                    trace.push(format!("\x1F\x0313{}", line));
334                    begin = end;
335                } else {
336                    trace.push(format!("\x0304{}", line));
337                }
338                end += 1;
339            }
340            // Start the trace where the panic actually occurred.
341            begin = if begin == 0 { 0 } else { begin - 1 };
342            hexchat.print(&trace[begin..end].join("\n"));
343        }
344    }));
345}