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}