// Hexchat Plugin API Bindings for Rust // Copyright (C) 2018, 2021, 2022 Soni L. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . //! Write hexchat plugins in Rust! //! //! This library provides safe API bindings for hexchat, but doesn't attempt to fix hexchat's own //! bugs. It makes no effort to stop you from unloading your own code while it's still running, for //! example. //! //! When using this library, it's strongly recommended to avoid heap-allocated statics (static //! mutexes, lazy_static, etc). This is because it's currently impossible to deallocate those on //! plugin unload. This can be worked around by placing those statics as fields in your plugin //! struct. //! //! This caveat does not apply to static assets (`static FOO: &'static str`, for example), but it //! does apply to thread-local storage. //! //! # Panics //! //! Unless otherwise stated, all functions in this crate taking strings (be //! that `&str`, `String`, etc) panic when the string contains embedded `NUL` //! characters (`\0`). //! //! # Examples //! //! ```no_run //! #[macro_use] //! extern crate hexchat_unsafe_plugin; //! //! use std::pin::Pin; //! use std::sync::Mutex; //! use hexchat_unsafe_plugin::{Plugin, PluginHandle, HookHandle}; //! //! #[derive(Default)] //! struct MyPlugin<'ph> { //! cmd: Mutex>> //! } //! //! unsafe impl<'ph> Plugin<'ph> for MyPlugin<'ph> { //! fn init(self: Pin<&mut Self>, ph: &mut PluginHandle<'ph>, filename: &str, arg: Option<&str>) -> bool { //! ph.register("myplugin", "0.1.0", "my simple plugin"); //! *self.cmd.lock().unwrap() = Some(ph.hook_command("hello-world", hexchat_unsafe_plugin::PRI_NORM, Some("prints 'Hello, World!'"), |ph, arg, arg_eol| { //! ph.print("Hello, World!"); //! hexchat_unsafe_plugin::EAT_ALL //! })); //! true //! } //! } //! //! hexchat_plugin!('ph, MyPlugin<'ph>); //! //! # fn main() { } // satisfy the compiler, we can't actually run the code //! ``` /* * Some info about how HexChat does things: * * All strings passed across the C API are UTF-8. * - Except `hexchat_get_info(ph, "libdirfs")`, so we need to be careful with * that one. * * The pointers * `name: *mut *const char, desc: *mut *const char, vers: *mut *const char` * point to inside the ph - that is, they're aliased. Thus, we must never * convert a ph to an & or &mut except as part of retrieving or setting values * in it (e.g. `(*ph).hexchat_get_info` or `(*ph).userdata = value`). * * `hexchat_plugin_get_info` is never used, so we omit it. It would be * impractical not to. * * These cause UB: * - `hexchat_command` may invalidate the plugin context. * - `hexchat_find_context` and `hexchat_nickcmp` use the plugin context * without checking it. * - `hexchat_get_prefs` uses the plugin context if name == "state_cursor" or * "id" (or anything with the same hash). * - `hexchat_list_get` uses the plugin context if name == "notify" (or * anything with the same hash). * - `hexchat_list_str`, `hexchat_list_int`, * - `hexchat_emit_print`, `hexchat_emit_print_attrs` use the plugin context. * - `hexchat_send_modes` uses the plugin context. * We need to wrap them (or, alternatively, hexchat_command). However, there's * no (safe) way to get a valid context afterwards. * - Actually that's a lie. Hexchat does a trick to give you a context as part * of the channel list. * We can use that to our advantage. I'm not sure if it's better to wrap * hexchat_command or the other functions, tho. * (Do we want to walk a linked list every time we use hexchat_command? * I'd think hexchat_command is the most used API function... Plus, * emit_print could indirectly invalidate the context as well.) * * `hexchat_send_modes` should only be used with channels; however, it doesn't * cause UB - it just doesn't work. * * `hexchat_pluginpref_get_int`, `hexchat_pluginpref_get_str`, * `hexchat_pluginpref_set_int`, `hexchat_pluginpref_set_str` cannot be used * while `name`, `desc`, `vers` are null. * * `hexchat_plugin_init` receives an arg string. it may be null. this isn't * documented anywhere. * * We can add the "close context" hook as the first thing when registering a * plugin, and invalidate known contexts from it. this is because hexchat * always adds new hooks with the same priority before old hooks. * * Borrowing can be scary. Things that can invalidate borrows must be &mut, but * borrows by themselves are generally fine. Unless they're not. * * Some get_info calls may be unsound on some platforms when threads are * involved, as they use getenv. This should be fixed by libc. We still need to * copy them into a String/CString. */ /* * Some info about how we do things: * * DO NOT CALL printf/commandf/etc FAMILY OF FUNCTIONS. You can't avoid allocations, so just * allocate some CStrings on the Rust side. It has the exact same effect, since those functions * allocate internally anyway. */ /* * Big list o' TODO: * -[ ] Finish API support. [PRI-HIGH] * -[x] word * -[x] word_eol * -[x] HEXCHAT_PRI_{HIGHEST, HIGH, NORM, LOW, LOWEST} * -[x] HEXCHAT_EAT_{NONE, HEXCHAT, PLUGIN, ALL} * -[ ] HEXCHAT_FD_{READ, WRITE, EXCEPTION, NOTSOCKET} * -[x] hexchat_command (for commandf, use command(&format!("...")), it is equivalent.) * -[x] hexchat_print (for printf, use print(&format!("...")), it is equivalent.) * -[x] hexchat_emit_print * -[x] hexchat_emit_print_attrs * -[x] hexchat_send_modes * -[x] hexchat_nickcmp * -[x] hexchat_strip * -[x] ~~hexchat_free~~ not available - use Drop impls. * -[x] ~~hexchat_event_attrs_create~~ not available - converted as needed * -[x] ~~hexchat_event_attrs_free~~ not available - use Drop impls. * -[#] hexchat_get_info (with the below as separate methods) * -[x] libdirfs * -[ ] gtkwin_ptr * -[ ] win_ptr * -[x] hexchat_get_prefs * -[x] hexchat_list_get * -[x] ~~hexchat_list_fields~~ not available. no alternative provided. * -[x] hexchat_list_next * -[x] hexchat_list_str * -[x] hexchat_list_int * -[ ] hexchat_list_time * -[x] ~~hexchat_list_free~~ not available - use Drop impls. * -[x] hexchat_hook_command * -[ ] hexchat_hook_fd * -[x] hexchat_hook_print * -[x] hexchat_hook_print_attrs * -[#] hexchat_hook_server (implemented through _attrs) * -[x] hexchat_hook_server_attrs * -[x] hexchat_hook_timer * -[x] ~~hexchat_unhook~~ not available - use Drop impls * -[x] hexchat_find_context * -[x] hexchat_get_context * -[x] hexchat_set_context * -[x] hexchat_pluginpref_set_str * -[x] hexchat_pluginpref_get_str * -[x] ~~hexchat_pluginpref_set_int~~ not available - use `format!` * -[x] ~~hexchat_pluginpref_get_int~~ not available - use `str::parse` * -[x] hexchat_pluginpref_delete * -[x] hexchat_pluginpref_list * -[x] hexchat_plugingui_add * -[x] ~~hexchat_plugingui_remove~~ not available - use Drop impls. * -[x] Wrap contexts within something we keep track of. Mark them invalid when contexts are * closed. * -[x] Anchor closures on the stack using Rc. Many (most?) hooks are reentrant. As far as I * know, all of them need this. * -[x] Additionally, use a Cell for timers. * -[ ] ??? */ #![cfg_attr(feature="nightly_tests", feature(c_variadic))] #[macro_use] extern crate impl_trait; #[doc(hidden)] pub extern crate libc; // private macros /// Calls a function on a PluginHandle struct. macro_rules! ph_call { ($f:ident($ph:expr, $($args:expr),*)) => { ((*$ph.data.ph).$f)($ph.plugin, $($args),*) }; ($f:ident($ph:expr $(,)?)) => { ((*$ph.data.ph).$f)($ph.plugin) }; } mod eat; mod extra_tests; mod infoid; mod internals; pub mod list; #[doc(hidden)] #[cfg(feature="nightly_tests")] pub mod mock; mod pluginfo; mod strip; mod word; pub use eat::*; pub use infoid::InfoId; pub use strip::*; pub use word::*; use internals::HexchatEventAttrs as RawAttrs; use internals::Ph as RawPh; use pluginfo::PluginInfo; use std::borrow::Cow; use std::cell::Cell; use std::cell::RefCell; use std::collections::HashSet; use std::convert::TryInto; use std::ffi::{CString, CStr}; use std::fmt; use std::marker::PhantomData; use std::mem; use std::mem::ManuallyDrop; use std::mem::MaybeUninit; use std::panic::{AssertUnwindSafe, RefUnwindSafe, UnwindSafe, catch_unwind}; use std::pin::Pin; use std::ptr; use std::rc::Rc; use std::rc::Weak as RcWeak; use std::str::FromStr; use std::thread; use std::time::{SystemTime, UNIX_EPOCH, Duration}; #[doc(hidden)] pub use libc::{c_char, c_int, c_void, time_t}; // ****** // // PUBLIC // // ****** // // Consts // PRI_* /// Equivalent to HEXCHAT_PRI_HIGHEST pub const PRI_HIGHEST: i32 = 127; /// Equivalent to HEXCHAT_PRI_HIGH pub const PRI_HIGH: i32 = 64; /// Equivalent to HEXCHAT_PRI_NORM pub const PRI_NORM: i32 = 0; /// Equivalent to HEXCHAT_PRI_LOW pub const PRI_LOW: i32 = -64; /// Equivalent to HEXCHAT_PRI_LOWEST pub const PRI_LOWEST: i32 = -128; // Traits /// A hexchat plugin. /// /// # Safety /// /// Modern operating systems cannot deal with dynamic unloading when threads /// are involved, because we still haven't figured out how to track which code /// address started a syscall that spawned a thread for some reason, so there's /// no way for the dynamic loader to stop those threads when unloading. /// /// *Fortunately* we can just shift that responsibility onto the unsuspecting /// Rust user. Because Rust is a safe language, this makes writing plugins /// inherently unsafe! /// /// At least Unsafe Rust is still safer than writing C. So you have that going. /// /// TL;DR: Either don't use threads, or ensure they're dead in `Drop`/`deinit`. pub unsafe trait Plugin<'ph> { /// Called to initialize the plugin. fn init(self: Pin<&mut Self>, ph: &mut PluginHandle<'ph>, filename: &str, arg: Option<&str>) -> bool; /// Called to deinitialize the plugin. /// /// This is always called immediately prior to Drop::drop. /// /// # A note about unwinding /// /// Panics in deinit will prevent the plugin from being correctly unloaded! /// Be careful! fn deinit(self: Pin<&mut Self>, ph: &mut PluginHandle<'ph>) { let _ = ph; } } // Structs /// A `*mut RawPh` with a lifetime bolted to it. /// /// This allows us to enforce a non-`'static` lifetime on the `Plugin`. // this is NOT public API #[doc(hidden)] #[repr(transparent)] #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] pub struct LtPhPtr<'ph> { ph: *mut RawPh, // 'ph has to be invariant because RawPh is self-referential. // "ideally" we'd want `&'ph mut RawPh<'ph>`, tho the `*mut` above would've // had the same effect if `RawPh` were `RawPh<'ph>`. _lt: PhantomData &'ph ()>, } /// A hexchat plugin handle, used to register hooks and interact with hexchat. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::{PluginHandle}; /// /// fn init(ph: &mut PluginHandle) { /// ph.register("myplug", "0.1.0", "my awesome plug"); /// } /// ``` pub struct PluginHandle<'ph> { data: LtPhPtr<'ph>, plugin: *mut RawPh, contexts: Contexts, // Used for registration info: PluginInfo, } /// A setting value, returned by [`ValidContext::get_prefs`]. #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] pub enum PrefValue { String(String), Int(i32), Bool(bool), } mod valid_context { use std::ptr; use crate::list; use crate::PluginHandle; /// A `PluginHandle` operating on a valid context. /// /// Methods that require a valid plugin context are only available through /// this. Methods that may invalidate the plugin context also consume this. /// /// See also [`PluginHandle::ensure_valid_context`]. pub struct ValidContext<'a, 'ph: 'a> { // NOTE: NOT mut(!) pub(crate) ph: &'a PluginHandle<'ph>, fields: list::Fields<'a, 'ph, list::Contexts>, _hidden: (), } impl<'a, 'ph: 'a> ValidContext<'a, 'ph> { /// Wraps a PluginHandle in a ValidContext. /// /// # Safety /// /// The PluginHandle's context must be valid. pub(crate) unsafe fn new(ph: &'a mut PluginHandle<'ph>) -> Self { static CONTEXTS: list::Contexts = list::Contexts; Self { ph: &*ph, fields: list::Fields { context: &*ph, // this isn't actually documented but hexchat explicitly // supports passing in NULL for the current context. // this gives access to some properties that aren't // available through get_info. list: ptr::null_mut(), _t: &CONTEXTS, }, _hidden: (), } } } impl<'a, 'ph: 'a> std::ops::Deref for ValidContext<'a, 'ph> { type Target = list::Fields<'a, 'ph, list::Contexts>; fn deref(&self) -> &Self::Target { &self.fields } } } pub use valid_context::ValidContext; /// Event attributes. // TODO better docs. #[derive(Clone)] pub struct EventAttrs<'a> { /// Server time. #[deprecated(since="2.3.0", note="Use raw_server_time instead")] pub server_time: Option, /// Raw server time. Only does anything if `use_raw_server_time` is set. pub raw_server_time: libc::time_t, /// Whether to pass `server_time` or `raw_server_time` to hexchat. pub use_raw_server_time: bool, _dummy: PhantomData<&'a ()>, } /// A hook handle. /// /// Likes to get caught on stuff. Unhooks when dropped. #[must_use = "Hooks must be stored somewhere and are automatically unhooked on Drop"] pub struct HookHandle<'ph, 'f> where 'f: 'ph { ph: LtPhPtr<'ph>, hh: *const internals::HexchatHook, freed: Rc>, // this does actually store an Rc<...>, but on the other side of the FFI. _f: PhantomData>>, } /// A virtual plugin. #[must_use = "Virtual plugins must be stored somewhere and are automatically unregistered on Drop"] pub struct PluginEntryHandle<'ph> { ph: LtPhPtr<'ph>, entry: *const internals::PluginGuiHandle, } /// A context. #[derive(Clone)] pub struct Context<'ph> { contexts: Contexts, ctx: RcWeak<*const internals::HexchatContext>, _ph: PhantomData<&'ph RawPh>, } /// The error returned by [`PluginHandle::with_context`] when the context is /// not valid. // #[derive(Debug)] // doesn't work pub struct InvalidContextError(F); /// An iterable list of plugin pref names. pub struct PluginPrefList { inner: Vec, } /// An iterator over `PluginPrefList`. pub struct PluginPrefListIter<'a> { list: Option<&'a [u8]>, } // Enums /// Errors returned by `pluginpref_*` functions. #[derive(Debug)] pub enum PluginPrefError { /// The var contains a forbidden character: `[ ,=\n]` (space, comma, /// equals, newline), is too long (max 126 bytes), is empty, or contains /// trailing ASCII whitespace. /// /// Returned by anything that interacts with vars. `pluginpref_list` only /// returns these during iteration. InvalidVar, /// The value starts with space. /// /// Returned by `pluginpref_set`. InvalidValue, /// The input (var + value) was too long after encoding. /// /// Returned by `pluginpref_set`. TooLong, /// The returned value was not valid UTF-8. /// /// Returned by `pluginpref_get`. Utf8Error(::std::ffi::IntoStringError), /// The returned var was not valid UTF-8. /// /// Returned while iterating a `pluginpref_list`. VarUtf8Error(::std::str::Utf8Error), /// The operation failed. /// /// Returned by anything that interacts with pluginprefs. Iterating a /// `pluginpref_list` never returns this. Failed, } // ***** // // impls // // ***** // impl<'a> Iterator for PluginPrefListIter<'a> { type Item = Result<&'a str, PluginPrefError>; fn next(&mut self) -> Option { let mut splitter = self.list?.splitn(2, |x| *x == 0); let ret = splitter.next().unwrap(); let ret = test_pluginpref_var(ret).and_then(|_| { std::str::from_utf8(ret).map_err(|e| PluginPrefError::VarUtf8Error(e)) }); self.list = splitter.next(); Some(ret) } } impl<'a> IntoIterator for &'a PluginPrefList { type Item = Result<&'a str, PluginPrefError>; type IntoIter = PluginPrefListIter<'a>; fn into_iter(self) -> Self::IntoIter { PluginPrefListIter { list: self.inner.split_last().map(|(_, x)| x), } } } impl InvalidContextError { /// Returns the closure wrapped within this error. pub fn get_closure(self) -> F { self.0 } } impl_trait! { impl PrefValue { /// Projects this `PrefValue` as `&str`, if it is a `String`. Returns /// `None` otherwise. pub fn as_str(&self) -> Option<&str> { match self { &Self::String(ref s) => Some(s), _ => None, } } /// Projects this `PrefValue` as `i32`, if it is an `Int`. Returns /// `None` otherwise. pub fn as_i32(&self) -> Option { match self { &Self::Int(i) => Some(i), _ => None, } } /// Projects this `PrefValue` as `bool`, if it is a `Bool`. Returns /// `None` otherwise. pub fn as_bool(&self) -> Option { match self { &Self::Bool(b) => Some(b), _ => None, } } /// Consumes this `PrefValue` and attempts to take a `String` out of /// it. pub fn into_string(self) -> Option { match self { Self::String(s) => Some(s), _ => None, } } impl trait From { fn from(s: String) -> Self { Self::String(s) } } impl trait From { fn from(i: i32) -> Self { Self::Int(i) } } impl trait From { fn from(b: bool) -> Self { Self::Bool(b) } } } } impl<'ph, 'f> HookHandle<'ph, 'f> where 'f: 'ph { /// If this is a timer hook, returns whether the hook has expired. /// /// Otherwise, returns false. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::{HookHandle}; /// /// /// Remove timers that have expired. /// fn clean_up_timers(timers: &mut Vec>) { /// timers.retain(|timer| { /// !timer.expired() /// }); /// } /// ``` pub fn expired(&self) -> bool { self.freed.get() } } impl<'ph, 'f> Drop for HookHandle<'ph, 'f> where 'f: 'ph { fn drop(&mut self) { if self.freed.get() { // already free'd. return; } self.freed.set(true); unsafe { let b = ((*self.ph.ph).hexchat_unhook)(self.ph.ph, self.hh) as *mut HookUd<'f>; // we assume b is not null. this should be safe. // just drop it drop(Rc::from_raw(b)); } } } impl<'ph> Drop for PluginEntryHandle<'ph> { fn drop(&mut self) { unsafe { ((*self.ph.ph).hexchat_plugingui_remove)(self.ph.ph, self.entry); } } } impl_trait! { impl<'ph> Context<'ph> { impl trait UnwindSafe {} impl trait RefUnwindSafe {} impl trait Drop { fn drop(&mut self) { // check if we need to clean anything up if self.ctx.strong_count() == 1 && self.ctx.weak_count() == 1 { let strong = self.ctx.upgrade().unwrap(); self.contexts.borrow_mut().remove(&strong); } } } } } /// Logs a panic message. /// /// # Safety /// /// `ph` must be a valid pointer (see `std::ptr::read`). unsafe fn log_panic(ph: *mut RawPh, e: Box) { // if it's a &str or String, just print it if let Some(s) = e.downcast_ref::<&str>() { hexchat_print_str(ph, s, false); } else if let Some(s) = e.downcast_ref::() { hexchat_print_str(ph, &s, false); } else if let Some(s) = e.downcast_ref::>() { hexchat_print_str(ph, &s, false); } else { hexchat_print_str(ph, "couldn't log panic message", false); if let Err(e) = catch_unwind(AssertUnwindSafe(|| drop(e))) { // eprintln panics, hexchat_print_str doesn't. hexchat_print_str(ph, "ERROR: panicked while trying to log panic!", false); mem::forget(e); std::process::abort(); } } } /// Handles a hook panic at the C-Rust ABI boundary. /// /// # Safety /// /// `ph` must be a valid pointer (see `std::ptr::read`). unsafe fn call_hook_protected Eat + UnwindSafe>( ph: *mut RawPh, f: F ) -> Eat { match catch_unwind(f) { Result::Ok(v @ _) => v, Result::Err(e @ _) => { log_panic(ph, e); EAT_NONE } } } impl<'ph> PluginHandle<'ph> { /// Wraps the raw handle. /// /// # Safety /// /// `ph` must be a valid pointer (see `std::ptr::read`). unsafe fn new(data: LtPhPtr<'ph>, info: PluginInfo, contexts: Contexts) -> PluginHandle<'ph> { PluginHandle { data, plugin: data.ph, info, contexts } } /// Registers this hexchat plugin. This must be called exactly once when the plugin is loaded. /// /// # Panics /// /// This function panics if this plugin is already registered. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::PluginHandle; /// /// fn init(ph: &mut PluginHandle<'_>) { /// ph.register("foo", "0.1.0", "my foo plugin"); /// } /// ``` pub fn register(&mut self, name: &str, desc: &str, ver: &str) { assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't be registered"); unsafe { let info = self.info; if !(*info.name).is_null() || !(*info.desc).is_null() || !(*info.vers).is_null() { panic!("Attempt to re-register a plugin"); } let name = CString::new(name).unwrap(); let desc = CString::new(desc).unwrap(); let ver = CString::new(ver).unwrap(); // these shouldn't panic. if they do, we'll need to free them afterwards. (*info.name) = name.into_raw(); (*info.desc) = desc.into_raw(); (*info.vers) = ver.into_raw(); } } /// Returns this plugin's registered name. /// /// # Panics /// /// This function panics if this plugin is not registered. pub fn get_name(&self) -> &str { assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't be registered"); unsafe { let info = self.info; if !(*info.name).is_null() || !(*info.desc).is_null() || !(*info.vers).is_null() { std::str::from_utf8_unchecked(CStr::from_ptr(*info.name).to_bytes()) } else { panic!("Attempt to get the name of a plugin that was not yet registered."); } } } /// Returns this plugin's registered description. /// /// # Panics /// /// This function panics if this plugin is not registered. pub fn get_description(&self) -> &str { assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't be registered"); unsafe { let info = self.info; if !(*info.name).is_null() || !(*info.desc).is_null() || !(*info.vers).is_null() { std::str::from_utf8_unchecked(CStr::from_ptr(*info.desc).to_bytes()) } else { panic!("Attempt to get the description of a plugin that was not yet registered."); } } } /// Returns this plugin's registered version. /// /// # Panics /// /// This function panics if this plugin is not registered. pub fn get_version(&self) -> &str { assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't be registered"); unsafe { let info = self.info; if !(*info.name).is_null() || !(*info.desc).is_null() || !(*info.vers).is_null() { std::str::from_utf8_unchecked(CStr::from_ptr(*info.vers).to_bytes()) } else { panic!("Attempt to get the version of a plugin that was not yet registered."); } } } /// Ensures the current context is valid. /// /// # Panics /// /// This function may panic if it's called while hexchat is closing. pub fn ensure_valid_context(&mut self, f: F) -> R where F: for<'a> FnOnce(ValidContext<'a, 'ph>) -> R { let ctx = self.get_context(); let res = self.with_context(&ctx, f); match res { Result::Ok(r @ _) => r, Result::Err(e @ _) => { let nctx = self.find_valid_context().expect("ensure_valid_context failed (find_valid_context failed), was hexchat closing?"); self.with_context(&nctx, e.get_closure()).ok().expect("ensure_valid_context failed, was hexchat closing?") } } } /// Operates on a virtual plugin. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::{PluginHandle}; /// /// fn contexts_can_be_passed_around(ph: &mut PluginHandle<'_>) { /// let ctx = ph.get_context(); /// let mut vplug = ph.plugingui_add("foo", "bar", "baz", "qux"); /// ph.for_entry_mut(&mut vplug, |ph| { /// ph.set_context(&ctx); /// ph.get_context() /// }); /// } /// ``` pub fn for_entry_mut(&mut self, entry: &mut PluginEntryHandle<'ph>, f: F) -> R where F: FnOnce(&mut PluginHandle<'ph>) -> R { // we're doing something kinda (very) weird here, but this is the only // way to get and set pluginprefs for virtual plugins. (not that we // support those yet...) // this should be sound? let data = self.data; let info = self.info; let contexts = Rc::clone(&self.contexts); let mut handle = unsafe { Self::new(data, info, contexts) }; handle.plugin = entry.entry as *mut RawPh; handle.set_context(&self.get_context()); f(&mut handle) } /// Registers a virtual plugin. pub fn plugingui_add(&self, filename: &str, name: &str, description: &str, version: &str) -> PluginEntryHandle<'ph> { let filename = CString::new(filename).unwrap(); let name = CString::new(name).unwrap(); let description = CString::new(description).unwrap(); let version = CString::new(version).unwrap(); let res = unsafe { ph_call!(hexchat_plugingui_add(self, filename.as_ptr(), name.as_ptr(), description.as_ptr(), version.as_ptr(), ptr::null_mut())) }; PluginEntryHandle { ph: self.data, entry: res } } /// Returns the current context. /// /// Note: The returned context may be invalid. Use [`Self::set_context`] to /// check. pub fn get_context(&self) -> Context<'ph> { let ctxp = unsafe { ph_call!(hexchat_get_context(self)) }; // This needs to be fixed by hexchat. I cannot make the value become // null when it's invalid without invoking UB. This is because I can't // set_context to null. let ok = unsafe { ph_call!(hexchat_set_context(self, ctxp)) }; unsafe { wrap_context(self, if ok == 0 { ptr::null() } else { ctxp }) } } /// Sets the current context. /// /// Returns `true` if the context is valid, `false` otherwise. pub fn set_context(&mut self, ctx: &Context<'ph>) -> bool { // we could've made this &self but we didn't. avoid breaking API. self.set_context_internal(ctx) } /// Do something in a valid context. /// /// Note that this changes the active context and doesn't change it back /// (but that should be fine for most use-cases). /// /// # Errors /// /// Returns `Err(InvalidContextError(f))` if the context is invalid. See /// [`set_context`]. Otherwise, returns `Ok(f(...))`. /// /// Note that `InvalidContextError` contains the original closure. This /// allows you to retry, if you so wish. /// /// [`set_context`]: #method.set_context #[inline] pub fn with_context(&mut self, ctx: &Context<'ph>, f: F) -> Result> where F: for<'a> FnOnce(ValidContext<'a, 'ph>) -> R { if !self.set_context(ctx) { Err(InvalidContextError(f)) } else { Ok(f(unsafe { ValidContext::new(self) })) } } /// Sets a command hook. /// /// # Panics /// /// Panics if this is a borrowed [`PluginEntryHandle`]. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle}; /// /// fn register_commands<'ph>(ph: &mut PluginHandle<'ph>) -> Vec> { /// vec![ /// ph.hook_command("hello-world", hexchat_unsafe_plugin::PRI_NORM, Some("prints 'Hello, World!'"), |ph, arg, arg_eol| { /// ph.print("Hello, World!"); /// hexchat_unsafe_plugin::EAT_ALL /// }), /// ] /// } /// ``` pub fn hook_command<'f, F>(&self, cmd: &str, pri: i32, help: Option<&str>, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'f + RefUnwindSafe, 'f: 'ph { assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks"); unsafe extern "C" fn callback(word: *const *const c_char, word_eol: *const *const c_char, ud: *mut c_void) -> c_int { let f: Rc = rc_clone_from_raw(ud as *const HookUd); (f)(word, word_eol, ptr::null()).do_eat as c_int } let b: Rc = { let ph = self.data; let info = self.info; let contexts = Rc::clone(&self.contexts); Rc::new(Box::new(move |word, word_eol, _| { let cb = &cb; let contexts = Rc::clone(&contexts); unsafe { call_hook_protected(ph.ph, move || { let mut ph = PluginHandle::new(ph, info, contexts); let word = Word::new(word); let word_eol = WordEol::new(word_eol); cb(&mut ph, word, word_eol) }) } })) }; let name = CString::new(cmd).unwrap(); let help_text = help.map(CString::new).map(Result::unwrap); let bp = Rc::into_raw(b); unsafe { let res = ph_call!(hexchat_hook_command(self, name.as_ptr(), pri as c_int, callback, help_text.as_ref().map(|s| s.as_ptr()).unwrap_or(ptr::null()), bp as *mut _)); assert!(!res.is_null()); HookHandle { ph: self.data, hh: res, freed: Default::default(), _f: PhantomData } } } /// Sets a server hook. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle}; /// /// fn register_server_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec> { /// vec![ /// ph.hook_server("PRIVMSG", hexchat_unsafe_plugin::PRI_NORM, |ph, word, word_eol| { /// if word.len() > 0 && word[0].starts_with('@') { /// ph.print("We have message tags!?"); /// } /// hexchat_unsafe_plugin::EAT_NONE /// }), /// ] /// } /// ``` pub fn hook_server<'f, F>(&self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'f + RefUnwindSafe, 'f: 'ph { assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks"); self.hook_server_attrs(cmd, pri, move |ph, w, we, _| cb(ph, w, we)) } /// Sets a server hook, with attributes. pub fn hook_server_attrs<'f, F>(&self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol, EventAttrs) -> Eat + 'f + RefUnwindSafe, 'f: 'ph { assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks"); unsafe extern "C" fn callback(word: *const *const c_char, word_eol: *const *const c_char, attrs: *const RawAttrs, ud: *mut c_void) -> c_int { let f: Rc = rc_clone_from_raw(ud as *const HookUd); (f)(word, word_eol, attrs).do_eat as c_int } let b: Rc = { let ph = self.data; let info = self.info; let contexts = Rc::clone(&self.contexts); Rc::new(Box::new(move |word, word_eol, attrs| { let cb = &cb; let contexts = Rc::clone(&contexts); unsafe { call_hook_protected(ph.ph, move || { let mut ph = PluginHandle::new(ph, info, contexts); let word = Word::new(word); let word_eol = WordEol::new(word_eol); let attrs = (&*attrs).into(); cb(&mut ph, word, word_eol, attrs) }) } })) }; let name = CString::new(cmd).unwrap(); let bp = Rc::into_raw(b); unsafe { let res = ph_call!(hexchat_hook_server_attrs(self, name.as_ptr(), pri as c_int, callback, bp as *mut _)); assert!(!res.is_null()); HookHandle { ph: self.data, hh: res, freed: Default::default(), _f: PhantomData } } } /// Sets a print hook. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle}; /// /// fn register_print_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec> { /// vec![ /// ph.hook_print("Channel Message", hexchat_unsafe_plugin::PRI_NORM, |ph, arg| { /// if let Some(nick) = arg.get(0) { /// if *nick == "KnOwN_SpAmMeR" { /// return hexchat_unsafe_plugin::EAT_ALL /// } /// } /// hexchat_unsafe_plugin::EAT_NONE /// }), /// ] /// } /// ``` pub fn hook_print<'f, F>(&self, name: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word) -> Eat + 'f + RefUnwindSafe, 'f: 'ph { assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks"); // hmm, is there any way to avoid this code duplication? // hook_print is special because dummy prints (keypresses, Close Context) are handled // through here, but never through hook_print_attrs. :/ unsafe extern "C" fn callback(word: *const *const c_char, ud: *mut c_void) -> c_int { let f: Rc = rc_clone_from_raw(ud as *const HookUd); (f)(word, ptr::null(), ptr::null()).do_eat as c_int } // we use "Close Context" to clean up dangling pointers. // hexchat lets plugins/hooks eat "Close Context" from eachother (tho // not from hexchat, for good reason), so this can still be unsound // if other plugins eat "Close Context". in those cases, we'd consider // those plugins to be faulty, tho it'd be nice if hexchat handled it. // we still do our best to be well-behaved. let suppress_eat = name.eq_ignore_ascii_case("Close Context"); let b: Rc = { let ph = self.data; let info = self.info; let contexts = Rc::clone(&self.contexts); Rc::new(Box::new(move |word, _, _| { let cb = &cb; let contexts = Rc::clone(&contexts); unsafe { call_hook_protected(ph.ph, move || { let mut ph = PluginHandle::new(ph, info, contexts); let word = Word::new(word); match cb(&mut ph, word) { _ if suppress_eat => EAT_NONE, eat => eat, } }) } })) }; let name = CString::new(name).unwrap(); let bp = Rc::into_raw(b); unsafe { let res = ph_call!(hexchat_hook_print(self, name.as_ptr(), pri as c_int, callback, bp as *mut _)); assert!(!res.is_null()); HookHandle { ph: self.data, hh: res, freed: Default::default(), _f: PhantomData } } } /// Sets a print hook, with attributes. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle}; /// /// fn register_print_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec> { /// vec![ /// ph.hook_print_attrs("Channel Message", hexchat_unsafe_plugin::PRI_NORM, |ph, arg, attrs| { /// if let Some(nick) = arg.get(0) { /// if *nick == "KnOwN_SpAmMeR" { /// return hexchat_unsafe_plugin::EAT_ALL /// } /// } /// hexchat_unsafe_plugin::EAT_NONE /// }), /// ] /// } /// ``` pub fn hook_print_attrs<'f, F>(&self, name: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, EventAttrs) -> Eat + 'f + RefUnwindSafe, 'f: 'ph { assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks"); unsafe extern "C" fn callback(word: *const *const c_char, attrs: *const RawAttrs, ud: *mut c_void) -> c_int { let f: Rc = rc_clone_from_raw(ud as *const HookUd); (f)(word, ptr::null(), attrs).do_eat as c_int } let b: Rc = { let ph = self.data; let info = self.info; let contexts = Rc::clone(&self.contexts); Rc::new(Box::new(move |word, _, attrs| { let cb = &cb; let contexts = Rc::clone(&contexts); unsafe { call_hook_protected(ph.ph, move || { let mut ph = PluginHandle::new(ph, info, contexts); let word = Word::new(word); let attrs = (&*attrs).into(); cb(&mut ph, word, attrs) }) } })) }; let name = CString::new(name).unwrap(); let bp = Rc::into_raw(b); unsafe { let res = ph_call!(hexchat_hook_print_attrs(self, name.as_ptr(), pri as c_int, callback, bp as *mut _)); assert!(!res.is_null()); HookHandle { ph: self.data, hh: res, freed: Default::default(), _f: PhantomData } } } /// Sets a timer hook /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle}; /// /// fn register_timers<'ph>(ph: &mut PluginHandle<'ph>) -> Vec> { /// vec![ /// ph.hook_timer(2000, |ph| { /// ph.print("timer up!"); /// false /// }), /// ] /// } /// ``` pub fn hook_timer<'f, F>(&self, timeout: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>) -> bool + 'f + RefUnwindSafe, 'f: 'ph { assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks"); unsafe extern "C" fn callback(ud: *mut c_void) -> c_int { let f: Rc = rc_clone_from_raw(ud as *const HookUd); (f)(ptr::null(), ptr::null(), ptr::null()).do_eat as c_int } let freed = Rc::new(Cell::new(false)); // helps us clean up the thing when returning false let dropper = Rc::new(Cell::new(None)); let b: Rc = { let ph = self.data; let info = self.info; let contexts = Rc::clone(&self.contexts); let freed = AssertUnwindSafe(Rc::clone(&freed)); let dropper = AssertUnwindSafe(Rc::clone(&dropper)); Rc::new(Box::new(move |_, _, _| { let cb = &cb; let contexts = Rc::clone(&contexts); let res = unsafe { call_hook_protected(ph.ph, move || { let mut ph = PluginHandle::new(ph, info, contexts); if cb(&mut ph) { EAT_HEXCHAT } else { EAT_NONE } }) }; if res == EAT_NONE && !freed.get() { freed.set(true); unsafe { // drop may panic // and so may unwrap // but we must not panic // (kinda silly to abuse call_hook_protected here // but hey, it works and it helps with stuff) call_hook_protected(ph.ph, || { drop(Rc::from_raw(dropper.take().unwrap())); EAT_NONE }); } } res })) }; let bp = Rc::into_raw(b); dropper.set(Some(bp)); unsafe { let res = ph_call!(hexchat_hook_timer(self, timeout as c_int, callback, bp as *mut _)); assert!(!res.is_null()); HookHandle { ph: self.data, hh: res, freed: freed, _f: PhantomData } } } /// Prints to the hexchat buffer. // this checks the context internally. if it didn't, it wouldn't be safe to // have here. pub fn print(&self, s: T) { let s = s.to_string(); unsafe { hexchat_print_str(self.data.ph, &*s, true); } } /// Prints to this hexchat buffer. /// /// Glue for usage of the [`write!`] macro with hexchat. /// /// # Panics /// /// This panics if any broken formatting trait implementations are used in /// the format arguments. See also [`format!`]. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::PluginHandle; /// /// fn hello_world(ph: &mut PluginHandle<'_>) { /// write!(ph, "Hello, world!"); /// } /// ``` pub fn write_fmt(&self, fmt: fmt::Arguments<'_>) { if let Some(s) = fmt.as_str() { // "fast" path. hexchat_print_str still has to allocate, and // hexchat is slow af. unsafe { hexchat_print_str(self.data.ph, s, true); } } else { self.print(fmt); } } /// Returns context and client information. /// /// See [`InfoId`] for the kinds of information that can be retrieved. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::{InfoId, PluginHandle}; /// /// /// Returns whether we are currently away. /// fn is_away(ph: &mut PluginHandle<'_>) -> bool { /// ph.get_info(InfoId::Away).is_some() /// } /// ``` pub fn get_info<'a>(&'a self, id: InfoId) -> Option> { let id_cstring = CString::new(&*id.name()).unwrap(); unsafe { let res = ph_call!(hexchat_get_info(self, id_cstring.as_ptr())); if res.is_null() { None } else { let s = CStr::from_ptr(res).to_str(); // FIXME: figure out which InfoId's are safe to borrow. Some(s.expect("broken hexchat").to_owned().into()) } } } /// Returns the path to the plugin directory, used for auto-loading /// plugins. /// /// The returned `CStr` is not guaranteed to be valid UTF-8, but local /// file system encoding. /// /// # Examples /// /// ```no_run /// use std::path::PathBuf; /// /// use hexchat_unsafe_plugin::PluginHandle; /// /// // On Unix, we can treat this as an array-of-bytes filesystem path. /// #[cfg(unix)] /// fn plugin_dir(ph: &PluginHandle<'_>) -> PathBuf { /// use std::ffi::OsString; /// use std::os::unix::ffi::OsStringExt; /// /// let libdirfs = ph.get_libdirfs().expect("should exist"); /// OsString::from_vec(libdirfs.into_bytes()).into() /// } /// ``` pub fn get_libdirfs(&self) -> Option { let id_cstring = CString::new("libdirfs").unwrap(); unsafe { let res = ph_call!(hexchat_get_info(self, id_cstring.as_ptr())); if res.is_null() { None } else { Some(CStr::from_ptr(res).to_owned()) } } } /// Strips attributes from text. See [`Strip`] for which attributes can /// be stripped. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::{PluginHandle, Strip}; /// /// /// Removes colors /// fn strip_colors(ph: &PluginHandle<'_>, s: &str) -> String { /// ph.strip(s, Strip::new().colors(true)) /// } /// ``` #[cfg(feature = "nul_in_strip")] pub fn strip(&self, s: &str, strip: Strip) -> String { // ironically the single API where we can pass embedded NULs. // we also don't need to worry about terminating it with NUL. let mut out = Vec::with_capacity(s.len()); let in_ptr = s.as_ptr() as *const _; let in_len = s.len().try_into().unwrap(); let flags = strip.flags(); // NOTE: avoid panicking from here. let stripped = unsafe { ph_call!(hexchat_strip(self, in_ptr, in_len, flags)) }; // tho annoyingly we don't know the out len, so we need to count NULs. let in_nuls = s.as_bytes().into_iter().filter(|&&x| x == 0).count(); let mut out_len = 0; for _ in 0..=in_nuls { while unsafe { *stripped.add(out_len) } != 0 { out_len += 1; } out_len += 1; } out.extend_from_slice(unsafe { // take out the extra NUL at the end. std::slice::from_raw_parts(stripped as *const _, out_len - 1) }); unsafe { ph_call!(hexchat_free(self, stripped as *const _)); } // we can panic again here, tho this should never panic. String::from_utf8(out).unwrap() } /// Strips attributes from text. See [`Strip`] for which attributes can /// be stripped. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::{PluginHandle, Strip}; /// /// /// Removes colors /// fn strip_colors(ph: &PluginHandle<'_>, s: &str) -> String { /// ph.strip(s, Strip::new().colors(true)) /// } /// ``` #[cfg(not(feature = "nul_in_strip"))] pub fn strip(&self, s: &str, strip: Strip) -> String { // make sure s doesn't contain nuls assert!(!s.as_bytes().contains(&0), "embedded nuls are not allowed"); let mut out = Vec::with_capacity(s.len()); let in_ptr = s.as_ptr() as *const _; let in_len = s.len().try_into().unwrap(); let flags = strip.flags(); // NOTE: avoid panicking from here. let stripped = unsafe { ph_call!(hexchat_strip(self, in_ptr, in_len, flags)) }; out.extend_from_slice(unsafe { CStr::from_ptr(stripped) }.to_bytes()); unsafe { ph_call!(hexchat_free(self, stripped as *const _)); } // we can panic again here, tho this should never panic. String::from_utf8(out).unwrap() } /// Sets a pluginpref. /// /// Note: If two pluginprefs exist with the same name, but different ASCII /// case, only one will be available through `pluginpref_get`. /// /// # Panics /// /// Panics if the plugin is not registered. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::PluginHandle; /// /// fn set_str(ph: &PluginHandle<'_>, val: &str) { /// ph.pluginpref_set("string", val); /// } /// /// fn set_int(ph: &PluginHandle<'_>, val: i32) { /// ph.pluginpref_set("int", &format!("{}", val)); /// } /// ``` pub fn pluginpref_set( &self, var: &str, value: &str, ) -> Result<(), PluginPrefError> { assert!(!self.info.name.is_null(), "must register plugin first"); if value.starts_with(' ') { return Err(PluginPrefError::InvalidValue) } let var_len = var.len(); let var = check_pluginpref_var(var)?; let mut val_len = value.len(); // octal is \000 - \377, adds 3 bytes val_len += 3 * value.bytes().filter(|&x| x < 32 || x > 127).count(); // special chars are \t \n etc, or octal - 2 bytes val_len -= 2 * value.bytes().filter(|&x| { matches!( x, | b'\n' | b'\r' | b'\t' | b'\x0C' // \f | b'\x08' // \b ) }).count(); // additionally \ and " also get an extra byte val_len += value.bytes().filter(|&x| x == b'"' || x == b'\\').count(); // 3 bytes from " = ", < 512 because 511 + NUL if var_len + val_len + 3 < 512 { let value = CString::new(value).unwrap(); let success = unsafe { ph_call!( hexchat_pluginpref_set_str( self, var.as_ptr(), value.as_ptr() ) ) } != 0; if !success { Err(PluginPrefError::Failed) } else { Ok(()) } } else { Err(PluginPrefError::TooLong) } } /// Retrieves a pluginpref. /// /// # Panics /// /// Panics if the plugin is not registered. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::PluginHandle; /// /// fn get_str(ph: &PluginHandle<'_>) -> String { /// ph.pluginpref_get("string").unwrap_or(String::new()) /// } /// /// fn get_int(ph: &PluginHandle<'_>) -> i32 { /// ph.pluginpref_get("int").unwrap_or(String::new()).parse().unwrap_or(-1) /// } /// ``` pub fn pluginpref_get( &self, var: &str, ) -> Result { assert!(!self.info.name.is_null(), "must register plugin first"); let var = check_pluginpref_var(var)?; let mut buffer: [MaybeUninit; 512] = unsafe { MaybeUninit::uninit().assume_init() }; let success = unsafe { ph_call!( hexchat_pluginpref_get_str( self, var.as_ptr(), buffer.as_mut_ptr() as *mut _ ) ) } != 0; if !success { Err(PluginPrefError::Failed) } else { match unsafe { CStr::from_ptr(buffer.as_ptr() as *const _) }.to_owned().into_string() { Ok(s) => Ok(s), Err(e) => Err(PluginPrefError::Utf8Error(e)), } } } /// Removes a pluginpref. /// /// # Panics /// /// Panics if the plugin is not registered. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::PluginHandle; /// /// fn del_str(ph: &PluginHandle<'_>) { /// let _ = ph.pluginpref_delete("string"); /// } /// /// fn del_int(ph: &PluginHandle<'_>) { /// let _ = ph.pluginpref_delete("int"); /// } /// ``` pub fn pluginpref_delete(&self, var: &str) -> Result<(), PluginPrefError> { assert!(!self.info.name.is_null(), "must register plugin first"); let var = check_pluginpref_var(var)?; let success = unsafe { ph_call!(hexchat_pluginpref_delete(self, var.as_ptr())) } != 0; if !success { Err(PluginPrefError::Failed) } else { Ok(()) } } /// Lists pluginprefs. /// /// # Panics /// /// Panics if the plugin is not registered. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::PluginHandle; /// /// fn list_prefs(ph: &PluginHandle<'_>) { /// match ph.pluginpref_list() { /// Ok(it) => for pref in &it { /// match pref { /// Ok(pref) => write!(ph, "{}", pref), /// _ => (), /// } /// }, /// _ => (), /// } /// } /// ``` pub fn pluginpref_list(&self) -> Result { assert!(!self.info.name.is_null(), "must register plugin first"); let mut buffer: [MaybeUninit; 4096] = unsafe { MaybeUninit::uninit().assume_init() }; let success = unsafe { ph_call!( hexchat_pluginpref_list(self, buffer.as_mut_ptr() as *mut _) ) } != 0; if !success { Err(PluginPrefError::Failed) } else { let mut list = PluginPrefList { inner: unsafe { CStr::from_ptr(buffer.as_ptr() as *const _) }.to_owned().into_bytes(), }; list.inner.iter_mut().for_each(|x| if *x == b',' { *x = 0; }); Ok(list) } } // ******* // // PRIVATE // // ******* // fn find_valid_context(&self) -> Option> { unsafe { let channel_key = cstr(b"channels\0"); let context_key = cstr(b"context\0"); // TODO wrap this in a safer API, with proper Drop #[allow(unused_mut)] let mut list = ph_call!(hexchat_list_get(self, channel_key)); // hexchat does this thing where it puts a context in a list_str. // this *is* the proper way to do this, but it looks weird. let ctx = if ph_call!(hexchat_list_next(self, list)) != 0 { Some(ph_call!(hexchat_list_str(self, list, context_key)) as *const internals::HexchatContext) } else { None }; ph_call!(hexchat_list_free(self, list)); ctx.map(|ctx| wrap_context(self, ctx)) } } /// Same as `set_context` but it takes `&self`. fn set_context_internal(&self, ctx: &Context<'ph>) -> bool { if let Some(ctx) = ctx.ctx.upgrade() { unsafe { ph_call!(hexchat_set_context(self, *ctx)) != 0 } } else { false } } } impl<'a> EventAttrs<'a> { /// Creates a new `EventAttrs`. #[allow(deprecated)] pub fn new() -> EventAttrs<'a> { EventAttrs { server_time: None, raw_server_time: 0, use_raw_server_time: false, _dummy: PhantomData, } } } impl<'a> From<&'a RawAttrs> for EventAttrs<'a> { #[allow(deprecated)] fn from(other: &'a RawAttrs) -> EventAttrs<'a> { EventAttrs { server_time: if other.server_time_utc > 0 { Some(UNIX_EPOCH + Duration::from_secs(other.server_time_utc as u64)) } else { None }, raw_server_time: other.server_time_utc, // Defaults to false for API compatibility. use_raw_server_time: false, _dummy: PhantomData, } } } impl<'a, 'ph: 'a> ValidContext<'a, 'ph> { /* * These cause UB: * `hexchat_command` may invalidate the plugin context. * `hexchat_find_context` and `hexchat_nickcmp` use the plugin context without checking it. * `hexchat_get_prefs` uses the plugin context if name == "state_cursor" or "id" (or anything with * the same hash). * `hexchat_list_get` uses the plugin context if name == "notify" (or anything with the same hash). * `hexchat_list_str`, `hexchat_list_int`, * `hexchat_emit_print`, `hexchat_emit_print_attrs` use the plugin context. * `hexchat_send_modes` uses the plugin context. * We need to wrap them (or, alternatively, hexchat_command). However, there's no (safe) way to get * a valid context afterwards. * - Actually that's a lie. Hexchat does a trick to give you a context as part of the channel list. * We can use that to our advantage. I'm not sure if it's better to wrap hexchat_command or the * other functions, tho. * (Do we want to walk a linked list every time we use hexchat_command? I'd think * hexchat_command is the most used API function... Plus, emit_print could indirectly * invalidate the context as well.) * * For performance we put them behind an ValidContext - things that don't invalidate the * context take an `&mut self`, things that do take an `self`. */ /// Finds an open context for the given servname and channel. pub fn find_context(&self, servname: Option<&str>, channel: Option<&str>) -> Option> { let ph = &self.ph; let servname = servname.map(|x| CString::new(x).unwrap()); let channel = channel.map(|x| CString::new(x).unwrap()); let ctx = unsafe { let sptr = servname.map(|x| x.as_ptr()).unwrap_or(ptr::null()); let cptr = channel.map(|x| x.as_ptr()).unwrap_or(ptr::null()); ph_call!(hexchat_find_context(ph, sptr, cptr)) }; if ctx.is_null() { None } else { Some(unsafe { wrap_context(self.ph, ctx) }) } } /// Compares two nicks based on the server's case mapping. /// /// ```no_run /// use hexchat_unsafe_plugin::ValidContext; /// /// /// Checks if the two nicks below are equal. /// fn compare_nicks(context: ValidContext<'_, '_>) -> bool { /// context.nickcmp("hello", "HELLO").is_eq() /// } /// ``` pub fn nickcmp(&self, nick1: &str, nick2: &str) -> std::cmp::Ordering { use std::cmp::Ordering; let ph = &self.ph; // need to put this in a more permanent position than temporaries let nick1 = CString::new(nick1).unwrap(); let nick2 = CString::new(nick2).unwrap(); let res = unsafe { ph_call!(hexchat_nickcmp(ph, nick1.as_ptr(), nick2.as_ptr())) }; if res < 0 { Ordering::Less } else if res > 0 { Ordering::Greater } else { Ordering::Equal } } /// Sends a list of modes to the current channel. pub fn send_modes<'b, I: IntoIterator>(&mut self, iter: I, mpl: i32, sign: char, mode: char) { let ph = &self.ph; assert!(sign == '+' || sign == '-', "sign must be + or -"); assert!(mode.is_ascii(), "mode must be ascii"); assert!(mpl >= 0, "mpl must be non-negative"); let v: Vec = iter.into_iter().map(|s| CString::new(s).unwrap()).collect(); let mut v2: Vec<*const c_char> = (&v).iter().map(|x| x.as_ptr()).collect(); let arr: &mut [*const c_char] = &mut *v2; unsafe { ph_call!(hexchat_send_modes(ph, arr.as_mut_ptr(), arr.len() as c_int, mpl as c_int, sign as c_char, mode as c_char)); } // some hexchat forks may invalidate the context here. // just pretend everything is fine and don't bump the major version. if !self.set_context(&self.get_context()) { let context = self.ph.find_valid_context().unwrap(); if !self.set_context(&context) { panic!(); } } } /// Returns a client setting. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::ValidContext; /// /// /// Returns the user's configured main nick. /// fn main_nick(context: ValidContext<'_, '_>) -> String { /// context.get_prefs("irc_nick1").unwrap().into_string().unwrap() /// } /// ``` pub fn get_prefs(&self, name: &str) -> Option { let ph = &self.ph; let name = CString::new(name).unwrap(); let mut string = 0 as *const c_char; let mut int: c_int = 0; match unsafe { ph_call!(hexchat_get_prefs(ph, name.as_ptr(), &mut string, &mut int)) } { 0 => None, 1 => Some(PrefValue::String(unsafe { CStr::from_ptr(string).to_owned().into_string().unwrap() })), 2 => Some(PrefValue::Int(int as _)), 3 => Some(PrefValue::Bool(int != 0)), _ => panic!("unsupported type in get_prefs"), } } /// Executes a command. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::{ValidContext}; /// /// fn join(context: ValidContext<'_, '_>, channel: &str) { /// context.command(&format!("join {}", channel)); /// } /// ``` pub fn command(self, cmd: &str) { let ph = self.ph; // need to put this in a more permanent position than temporaries let cmd = CString::new(cmd).unwrap(); unsafe { ph_call!(hexchat_command(ph, cmd.as_ptr())) } } /// Prints an event message, and returns a success status (whether or not /// the event exists). /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::{ValidContext}; /// /// fn emit_channel_message(context: ValidContext<'_, '_>, nick: &str, msg: &str) { /// context.emit_print("Channel Message", [nick, msg]); /// } /// ``` pub fn emit_print<'b, I: IntoIterator>(self, event: &str, args: I) -> bool { let ph = self.ph; let event = CString::new(event).unwrap(); let mut args_cs: [Option; 4] = [None, None, None, None]; { let mut iter = args.into_iter(); for i in 0..4 { args_cs[i] = iter.next().map(|x| CString::new(x).unwrap()); if args_cs[i].is_none() { break; } } if iter.next().is_some() { // it's better to panic so as to get bug reports when we need to increase this panic!("too many arguments to emit_print (max 4), or iterator not fused"); } } let mut argv: [*const c_char; 5] = [ptr::null(); 5]; for i in 0..4 { argv[i] = args_cs[i].as_ref().map_or(ptr::null(), |s| s.as_ptr()); } unsafe { ph_call!(hexchat_emit_print(ph, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4])) != 0 } } /// Prints an event message, and returns a success status (whether or not /// the event exists). /// /// Also allows setting event attributes. /// /// # Examples /// /// ```no_run /// use std::time::SystemTime; /// use hexchat_unsafe_plugin::{EventAttrs, ValidContext}; /// /// fn emit_channel_message_at(context: ValidContext<'_, '_>, time: Option, nick: &str, msg: &str) { /// let mut attrs = EventAttrs::new(); /// attrs.server_time = time; /// context.emit_print_attrs(attrs, "Channel Message", [nick, msg]); /// } /// ``` pub fn emit_print_attrs<'b, I: IntoIterator>(self, attrs: EventAttrs, event: &str, args: I) -> bool { let ph = self.ph; let event = CString::new(event).unwrap(); let mut args_cs: [Option; 4] = [None, None, None, None]; { let mut iter = args.into_iter(); for i in 0..4 { args_cs[i] = iter.next().map(|x| CString::new(x).unwrap()); if args_cs[i].is_none() { break; } } if let Some(_) = iter.next() { // it's better to panic so as to get bug reports when we need to increase this panic!("too many arguments to emit_print_attrs (max 4), or iterator not fused"); } } let mut argv: [*const c_char; 5] = [ptr::null(); 5]; for i in 0..4 { argv[i] = args_cs[i].as_ref().map_or(ptr::null(), |s| s.as_ptr()); } let helper = unsafe { HexchatEventAttrsHelper::new_with(ph, attrs) }; unsafe { ph_call!(hexchat_emit_print_attrs(ph, helper.0, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4])) != 0 } } /// Retrieves a list. /// /// # Examples /// /// ```no_run /// use hexchat_unsafe_plugin::list::Contexts; /// use hexchat_unsafe_plugin::PluginHandle; /// /// fn print_contexts(ph: &mut PluginHandle<'_>) { /// ph.ensure_valid_context(|ph| { /// let mut contexts = ph.list(&Contexts); /// while let Some(context) = contexts.next() { /// write!(ph, "{}", context.name().unwrap()); /// } /// }) /// } /// ``` pub fn list(&'a self, t: &'a T) -> list::Entries<'a, 'ph, T> { let ph = &self.ph; let list = CString::new(&*t.name()).unwrap(); let list = unsafe { ph_call!(hexchat_list_get(ph, list.as_ptr())) }; list::Entries { context: self.ph, list: list, t: t, } } // ******** // // FORWARDS // // ******** // // We can't just deref because then you could recursively ensure valid context and then it'd no // longer work. /// Returns the current context. /// /// See [`PluginHandle::get_context`]. pub fn get_context(&self) -> Context<'ph> { self.ph.get_context() } /// Sets the current context. /// /// Returns `true` if the context is valid, `false` otherwise. /// /// See [`PluginHandle::set_context`]. // One would think this is unsound, but if the given context isn't valid, // the current context is unchanged and still valid. // But if the given context is valid, it becomes the new, valid, current // context. // So either way we're still in a valid context when this returns. pub fn set_context(&mut self, ctx: &Context<'ph>) -> bool { self.ph.set_context_internal(ctx) } /// Prints to the hexchat buffer. /// /// See [`PluginHandle::print`]. pub fn print(&self, s: T) { self.ph.print(s) } /// Prints to the hexchat buffer. /// /// Glue for usage of the [`write!`] macro with hexchat. /// /// See [`PluginHandle::write_fmt`]. /// /// # Panics /// /// This panics if any broken formatting trait implementations are used in /// the format arguments. See also [`format!`]. pub fn write_fmt(&self, fmt: fmt::Arguments<'_>) { self.ph.write_fmt(fmt) } /// Sets a command hook. /// /// See [`PluginHandle::hook_command`]. pub fn hook_command<'f, F>(&self, cmd: &str, pri: i32, help: Option<&str>, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'f + RefUnwindSafe, 'f: 'ph { self.ph.hook_command(cmd, pri, help, cb) } /// Sets a server hook. /// /// See [`PluginHandle::hook_server`]. pub fn hook_server<'f, F>(&self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'f + RefUnwindSafe, 'f: 'ph { self.ph.hook_server(cmd, pri, cb) } /// Sets a server hook with attributes. /// /// See [`PluginHandle::hook_server_attrs`]. pub fn hook_server_attrs<'f, F>(&self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol, EventAttrs) -> Eat + 'f + RefUnwindSafe, 'f: 'ph { self.ph.hook_server_attrs(cmd, pri, cb) } /// Sets a print hook. /// /// See [`PluginHandle::hook_print`]. pub fn hook_print<'f, F>(&self, name: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word) -> Eat + 'f + RefUnwindSafe, 'f: 'ph { self.ph.hook_print(name, pri, cb) } /// Sets a print hook with attributes. /// /// See [`PluginHandle::hook_print_attrs`]. pub fn hook_print_attrs<'f, F>(&self, name: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, EventAttrs) -> Eat + 'f + RefUnwindSafe, 'f: 'ph { self.ph.hook_print_attrs(name, pri, cb) } /// Sets a timer hook. /// /// See [`PluginHandle::hook_timer`]. pub fn hook_timer<'f, F>(&self, timeout: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>) -> bool + 'f + RefUnwindSafe, 'f: 'ph { self.ph.hook_timer(timeout, cb) } /// Returns context and client information. /// /// See [`PluginHandle::get_info`]. pub fn get_info<'b>(&'b self, id: InfoId) -> Option> { self.ph.get_info(id) } /// Returns the plugin directory. /// /// See [`PluginHandle::get_libdirfs`]. pub fn get_libdirfs(&self) -> Option { self.ph.get_libdirfs() } /// Strips attributes from text. /// /// See [`PluginHandle::strip`]. pub fn strip(&self, s: &str, strip: Strip) -> String { self.ph.strip(s, strip) } /// Sets a pluginpref. /// /// See [`PluginHandle::pluginpref_set`]. pub fn pluginpref_set( &self, var: &str, value: &str, ) -> Result<(), PluginPrefError> { self.ph.pluginpref_set(var, value) } /// Retrieves a pluginpref. /// /// See [`PluginHandle::pluginpref_get`]. pub fn pluginpref_get( &self, var: &str, ) -> Result { self.ph.pluginpref_get(var) } /// Removes a pluginpref. /// /// See [`PluginHandle::pluginpref_delete`]. pub fn pluginpref_delete(&self, var: &str) -> Result<(), PluginPrefError> { self.ph.pluginpref_delete(var) } /// Lists pluginprefs. /// /// See [`PluginHandle::pluginpref_list`]. pub fn pluginpref_list(&self) -> Result { self.ph.pluginpref_list() } } // ******* // // PRIVATE // // ******* // // Type aliases, used for safety type checking. // /// Userdata type used by a command hook. // We actually do want RefUnwindSafe. This function may be called multiple times, and it's not // automatically invalidated if it panics, so it may be called again after it panics. If you need // mutable state, std provides std::sync::Mutex which has poisoning. Other interior mutability with // poisoning could also be used. std doesn't have anything for single-threaded performance (yet), // but hexchat isn't particularly performance-critical. // type CommandHookUd = Box Eat + std::panic::RefUnwindSafe>; // /// Userdata type used by a server hook. // type ServerHookUd = Box Eat + std::panic::RefUnwindSafe>; // /// Userdata type used by a print hook. // type PrintHookUd = Box Eat + std::panic::RefUnwindSafe>; // /// Userdata type used by a timer hook. // type TimerHookUd = Box bool + std::panic::RefUnwindSafe>; /// Userdata type used by a hook type HookUd<'f> = Box Eat + RefUnwindSafe + 'f>; /// Contexts type Contexts = Rc>>>>; /// The contents of an empty CStr. /// /// This is useful where you need a non-null CStr. // NOTE: MUST BE b"\0"! const EMPTY_CSTRING_DATA: &[u8] = b"\0"; /// Converts a nul-terminated const bstring to a C string. /// /// # Panics /// /// Panics if b contains embedded nuls. // TODO const fn, once that's possible fn cstr(b: &'static [u8]) -> *const c_char { CStr::from_bytes_with_nul(b).unwrap().as_ptr() } /// Clones an Rc straight from a raw pointer. /// /// # Safety /// /// This function is unsafe because `ptr` must hame come from `Rc::into_raw`. unsafe fn rc_clone_from_raw(ptr: *const T) -> Rc { let rc = ManuallyDrop::new(Rc::from_raw(ptr)); Rc::clone(&rc) } /// Converts a **valid** context pointer into a Context (Rust-managed) struct. /// /// # Safety /// /// This function doesn't validate the context. unsafe fn wrap_context<'ph>(ph: &PluginHandle<'ph>, ctx: *const internals::HexchatContext) -> Context<'ph> { let contexts = ph.contexts.clone(); if ctx.is_null() { Context { contexts, ctx: RcWeak::new(), _ph: PhantomData } } else { let weak_ctxp = (|| { // need to drop the borrow(), so use an (|| IIFE)() contexts.borrow().get(&ctx).map(|x| { Rc::downgrade(x) }) })().unwrap_or_else(|| { let ctxp = Rc::new(ctx); let weak_ctxp = Rc::downgrade(&ctxp); contexts.borrow_mut().insert(ctxp); weak_ctxp }); Context { contexts, ctx: weak_ctxp, _ph: PhantomData } } } /// Prints an &str to hexchat, trying to avoid allocations. /// /// # Safety /// /// This function does not check the passed in argument. /// /// # Panics /// /// Panics if panic_on_nul is true and the string contains embedded nuls. unsafe fn hexchat_print_str(ph: *mut RawPh, s: &str, panic_on_nul: bool) { match CString::new(s) { Result::Ok(cs @ _) => { let csr: &CStr = &cs; ((*ph).hexchat_print)(ph, csr.as_ptr()) }, e @ _ => if panic_on_nul {e.unwrap();}, // TODO nul_position? } } /// Helper to manage owned RawAttrs struct HexchatEventAttrsHelper<'a, 'ph>(*mut RawAttrs, &'a PluginHandle<'ph>) where 'ph: 'a; impl<'a, 'ph> HexchatEventAttrsHelper<'a, 'ph> where 'ph: 'a { /// Creates a new, empty `HexchatEventAttrsHelper`. /// /// # Safety /// /// `ph` must be a valid raw plugin handle. unsafe fn new(ph: &'a PluginHandle<'ph>) -> Self { HexchatEventAttrsHelper(ph_call!(hexchat_event_attrs_create(ph)), ph) } /// Creates a new `HexchatEventAttrsHelper` for a given `EventAttrs`. /// /// # Safety /// /// `ph` must be a valid raw plugin handle. unsafe fn new_with(ph: &'a PluginHandle<'ph>, attrs: EventAttrs<'_>) -> Self { let helper = Self::new(ph); #[allow(deprecated)] let v = if attrs.use_raw_server_time { attrs.raw_server_time } else { attrs.server_time.or(Some(UNIX_EPOCH)).map(|st| { match st.duration_since(UNIX_EPOCH) { Ok(n) => n.as_secs(), Err(_) => 0 } }).filter(|&st| { st < (time_t::max_value() as u64) }).unwrap() as time_t }; (*helper.0).server_time_utc = v; helper } } impl<'a, 'ph> Drop for HexchatEventAttrsHelper<'a, 'ph> where 'ph: 'a { fn drop(&mut self) { unsafe { ph_call!(hexchat_event_attrs_free(self.1, self.0)) } } } /// Plugin data stored in the hexchat plugin_handle. struct PhUserdata<'ph> { plug: Pin + 'ph>>, contexts: Contexts, // this is never read, but we need to not drop it until we can drop it _context_hook: HookHandle<'ph, 'ph>, pluginfo: PluginInfo, } /// Puts the userdata in the plugin handle. /// /// # Safety /// /// This function is unsafe because it doesn't check if the pointer is valid. /// /// Improper use of this function can leak memory. unsafe fn put_userdata<'ph>(ph: LtPhPtr<'ph>, ud: Box>) { (*ph.ph).userdata = Box::into_raw(ud) as *mut c_void; } /// Pops the userdata from the plugin handle. /// /// # Safety /// /// This function is unsafe because it doesn't check if the pointer is valid. unsafe fn pop_userdata<'ph>(ph: LtPhPtr<'ph>) -> Box> { Box::from_raw(mem::replace(&mut (*ph.ph).userdata, ptr::null_mut()) as *mut PhUserdata<'ph>) } fn test_pluginpref_var(var: &[u8]) -> Result<(), PluginPrefError> { if var.len() >= 127 || var.len() < 1 // rust uses the same definition of ascii whitespace || var.last().unwrap().is_ascii_whitespace() || var.contains(&b' ') || var.contains(&b'=') || var.contains(&b'\n') || var.contains(&b',') { Err(PluginPrefError::InvalidVar) } else { Ok(()) } } fn check_pluginpref_var(var: impl Into>) -> Result { let var = var.into(); test_pluginpref_var(&var)?; Ok(CString::new(var).unwrap()) } // *********************** // // PUBLIC OUT OF NECESSITY // // *********************** // #[doc(hidden)] pub unsafe fn hexchat_plugin_init<'ph, T>(plugin_handle: LtPhPtr<'ph>, plugin_name: *mut *const c_char, plugin_desc: *mut *const c_char, plugin_version: *mut *const c_char, arg: *const c_char) -> c_int where T: Plugin<'ph> + Default + 'ph { if plugin_handle.ph.is_null() || plugin_name.is_null() || plugin_desc.is_null() || plugin_version.is_null() { // we can't really do anything here. just hope this doesn't panic. eprintln!("hexchat_plugin_init called with a null pointer that shouldn't be null - broken hexchat"); std::process::abort(); } let ph = plugin_handle.ph as *mut RawPh; // clear the "userdata" field first thing - if the deinit function gets called (wrong hexchat // version, other issues), we don't wanna try to drop the hexchat_dummy or hexchat_read_fd // function as if it were a Box! (*ph).userdata = ptr::null_mut(); // read the filename so we can pass it on later. let filename = if !(*plugin_name).is_null() { if let Ok(fname) = CStr::from_ptr(*plugin_name).to_owned().into_string() { fname } else { hexchat_print_str(ph, "failed to convert filename to utf8 - broken hexchat", false); return 0; } } else { // no filename specified for some reason, but we can still load String::new() // empty string }; // these may be null, unless initialization is successful. // we set them to null as markers. *plugin_name = ptr::null(); *plugin_desc = ptr::null(); *plugin_version = ptr::null(); // do some version checks for safety // NOTE: calling hexchat functions with null plugin_name, plugin_desc, plugin_version is a bit // dangerous. this particular case is "ok". { let ver = ((*ph).hexchat_get_info)(ph, cstr(b"version\0")); // this shouldn't panic let cstr = CStr::from_ptr(ver); if let Ok(ver) = cstr.to_str() { let mut iter = ver.split('.'); let a = iter.next().map(i32::from_str).and_then(Result::ok).unwrap_or(0); let b = iter.next().map(i32::from_str).and_then(Result::ok).unwrap_or(0); let c = iter.next().map(i32::from_str).and_then(Result::ok).unwrap_or(0); // 2.9.6 or greater if !(a > 2 || (a == 2 && (b > 9 || (b == 9 && (c > 6 || (c == 6)))))) { return 0; } } else { return 0; } } // we got banned from an IRC network over the following line of code. hexchat_print_str(ph, "hexchat-unsafe-plugin Copyright (C) 2018, 2021, 2022 Soni L. GPL-3.0-or-later This software is made with love by a queer trans person.", false); let mut pluginfo = if let Some(pluginfo) = PluginInfo::new(plugin_name, plugin_desc, plugin_version) { pluginfo } else { return 0; }; let r: thread::Result>> = { catch_unwind(move || { // AssertUnwindSafe not Default at the time of writing this let contexts = Rc::new(AssertUnwindSafe(Default::default())); let mut pluginhandle = PluginHandle::new(plugin_handle, pluginfo, contexts); let contexts = Rc::clone(&pluginhandle.contexts); // must register this before the plugin registers anything else! let context_hook = pluginhandle.hook_print("Close Context", c_int::min_value(), move |ph, _| { // just remove the context! it's that simple! let ctx = ph_call!(hexchat_get_context(ph)); contexts.borrow_mut().remove(&ctx); EAT_NONE }); let contexts = Rc::clone(&pluginhandle.contexts); let mut plug = Box::pin(T::default()); if plug.as_mut().init(&mut pluginhandle, &filename, if !arg.is_null() { Some(CStr::from_ptr(arg).to_str().expect("arg not valid utf-8 - broken hexchat")) } else { None }) { if !(pluginfo.name.is_null() || pluginfo.desc.is_null() || pluginfo.vers.is_null()) { Some(Box::new(PhUserdata { plug, pluginfo, contexts, _context_hook: context_hook })) } else { // TODO log: forgot to call register None } } else { if !(pluginfo.name.is_null() || pluginfo.desc.is_null() || pluginfo.vers.is_null()) { pluginfo.drop_info() } None } }) }; match r { Result::Ok(Option::Some(plug @ _)) => { put_userdata(plugin_handle, plug); 1 }, r @ _ => { if let Err(e) = r { log_panic(ph, e); } 0 }, } } #[doc(hidden)] pub unsafe fn hexchat_plugin_deinit<'ph, T>(plugin_handle: LtPhPtr<'ph>) -> c_int where T: Plugin<'ph> { let mut safe_to_unload = 1; // plugin_handle should never be null, but just in case. if !plugin_handle.ph.is_null() { let ph = plugin_handle.ph as *mut RawPh; // userdata should also never be null. if !(*ph).userdata.is_null() { { let mut info: Option = None; { let mut ausinfo = AssertUnwindSafe(&mut info); safe_to_unload = match catch_unwind(move || { let mut userdata = pop_userdata(plugin_handle); let pluginfo = userdata.pluginfo; if let Err(e) = catch_unwind(AssertUnwindSafe(|| { userdata.plug.as_mut().deinit(&mut { PluginHandle::new( plugin_handle, pluginfo, Rc::clone(&userdata.contexts) ) }); })) { // panics in deinit may be retried. // however, one may need to close hexchat if that // happens. put_userdata(plugin_handle, userdata); std::panic::resume_unwind(e); } **ausinfo = Some(pluginfo); drop(userdata); }) { Ok(_) => 1, Err(e) => { log_panic(ph, e); 0 } }; } if let Some(mut info) = info { info.drop_info(); safe_to_unload = 1; } } } else { hexchat_print_str(ph, "plugin userdata was null, broken hexchat-unsafe-plugin?", false); } } else { // we are once again hoping for the best here. eprintln!("hexchat_plugin_deinit called with a null plugin_handle - broken hexchat"); std::process::abort(); } safe_to_unload } /// Exports a hexchat plugin. #[macro_export] macro_rules! hexchat_plugin { ($l:lifetime, $t:ty) => { #[no_mangle] pub unsafe extern "C" fn hexchat_plugin_init<$l>(plugin_handle: $crate::LtPhPtr<$l>, plugin_name: *mut *const $crate::c_char, plugin_desc: *mut *const $crate::c_char, plugin_version: *mut *const $crate::c_char, arg: *const $crate::c_char) -> $crate::c_int { $crate::hexchat_plugin_init::<$l, $t>(plugin_handle, plugin_name, plugin_desc, plugin_version, arg) } #[no_mangle] pub unsafe extern "C" fn hexchat_plugin_deinit<$l>(plugin_handle: $crate::LtPhPtr<$l>) -> $crate::c_int { $crate::hexchat_plugin_deinit::<$l, $t>(plugin_handle) } // unlike what the documentation states, there's no need to define hexchat_plugin_get_info. // so we don't. it'd be impossible to make it work well with rust anyway. }; }