diff options
author | SoniEx2 <endermoneymod@gmail.com> | 2022-04-15 23:49:31 -0300 |
---|---|---|
committer | SoniEx2 <endermoneymod@gmail.com> | 2022-04-15 23:49:31 -0300 |
commit | 2b65b6bda3a3467a088aba7ed31e5990971bfe31 (patch) | |
tree | 8f93e0abca0b9c726c5eecc1e6926cedfd2cd88a | |
parent | 52a1dc21f2fa6131458dcf9b303c595cb9ce6d42 (diff) |
Clean up, support virtual plugins, Pin, etc
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | src/lib.rs | 553 | ||||
-rw-r--r-- | src/strip.rs | 134 |
3 files changed, 541 insertions, 151 deletions
diff --git a/Cargo.toml b/Cargo.toml index 193c430..4c06152 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hexchat-unsafe-plugin" -version = "1.0.0" +version = "2.0.0" authors = ["SoniEx2 <endermoneymod@gmail.com>"] description = "Lets you write native HexChat plugins in mostly-safe Rust" license = "GPL-3.0-or-later" @@ -10,5 +10,8 @@ keywords = ["hexchat", "plugin", "hexchat-plugin"] categories = ["api-bindings"] readme = "README.md" +[features] +nul_in_strip = [] + [dependencies] libc = "0.2" diff --git a/src/lib.rs b/src/lib.rs index 4dc1bfe..f539a58 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,16 +34,17 @@ //! #[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<Option<HookHandle<'ph>>> +//! cmd: Mutex<Option<HookHandle<'ph, 'ph>>> //! } //! //! unsafe impl<'ph> Plugin<'ph> for MyPlugin<'ph> { -//! fn init(&self, ph: &mut PluginHandle<'ph>, filename: &str, arg: Option<&str>) -> bool { +//! 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!"); @@ -108,6 +109,13 @@ * 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. */ /* @@ -132,14 +140,22 @@ * -[x] hexchat_emit_print_attrs * -[x] hexchat_send_modes * -[x] hexchat_nickcmp - * -[ ] hexchat_strip + * -[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. - * -[x] hexchat_get_info + * -[#] hexchat_get_info (with the below as separate methods) + * -[x] libdirfs + * -[ ] gtkwin_ptr + * -[ ] win_ptr * -[ ] hexchat_get_prefs - * -[ ] hexchat_list_get, hexchat_list_fields, hexchat_list_next, hexchat_list_str, - * hexchat_list_int, hexchat_list_time, hexchat_list_free + * -[ ] hexchat_list_get + * -[ ] hexchat_list_fields + * -[ ] hexchat_list_next + * -[ ] hexchat_list_str + * -[ ] 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 @@ -157,10 +173,10 @@ * -[ ] hexchat_pluginpref_get_int * -[ ] hexchat_pluginpref_delete * -[ ] hexchat_pluginpref_list - * -[ ] hexchat_plugingui_add + * -[x] hexchat_plugingui_add * -[x] ~~hexchat_plugingui_remove~~ not available - use Drop impls. - * -[ ] Wrap contexts within something we keep track of. Mark them invalid when contexts are - * closed. [PRI-MAYBE] + * -[x] Wrap contexts within something we keep track of. Mark them invalid when contexts are + * closed. * -[x] Anchor closures on the stack using Rc<T>. Many (most?) hooks are reentrant. As far as I * know, all of them need this. * -[x] Additionally, use a Cell<bool> for timers. @@ -175,10 +191,12 @@ mod infoid; mod internals; mod pluginfo; mod word; +mod strip; pub use eat::*; pub use infoid::InfoId; pub use word::*; +pub use strip::*; use pluginfo::PluginInfo; use internals::Ph as RawPh; @@ -188,12 +206,14 @@ 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::panic::{AssertUnwindSafe, RefUnwindSafe, UnwindSafe, catch_unwind}; +use std::pin::Pin; use std::ptr; use std::rc::Rc; use std::rc::Weak as RcWeak; @@ -204,6 +224,18 @@ use std::time::{SystemTime, UNIX_EPOCH, Duration}; #[doc(hidden)] pub use libc::{c_char, c_int, c_void, time_t}; +// 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) + }; +} + // ****** // // PUBLIC // // ****** // @@ -238,14 +270,16 @@ pub const PRI_LOWEST: i32 = -128; /// 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, ph: &mut PluginHandle<'ph>, filename: &str, arg: Option<&str>) -> bool; + 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. - fn deinit(&self, ph: &mut PluginHandle<'ph>) { + fn deinit(self: Pin<&mut Self>, ph: &mut PluginHandle<'ph>) { let _ = ph; } } @@ -255,12 +289,12 @@ pub unsafe trait Plugin<'ph> { /// A `*mut RawPh` with a lifetime bolted to it. /// /// This allows us to enforce a non-`'static` lifetime on the `Plugin`. -#[repr(transparent)] +// 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, - // FIXME we may want a different signature here (&'a Cell<RawPh>?) _lt: PhantomData<&'ph RawPh>, } @@ -276,7 +310,8 @@ pub struct LtPhPtr<'ph> { /// } /// ``` pub struct PluginHandle<'ph> { - ph: LtPhPtr<'ph>, + data: LtPhPtr<'ph>, + plugin: *mut RawPh, contexts: Contexts, // Used for registration info: PluginInfo, @@ -320,12 +355,19 @@ pub struct EventAttrs<'a> { /// A hook handle, used to enable unhooking. #[must_use = "Hooks must be stored somewhere and are automatically unhooked on Drop"] -pub struct HookHandle<'ph> { +pub struct HookHandle<'ph, 'f> where 'ph: 'f { ph: LtPhPtr<'ph>, hh: *const internals::HexchatHook, freed: Rc<Cell<bool>>, // this does actually store an Rc<...>, but on the other side of the FFI. - _f: PhantomData<Rc<HookUd<'ph>>>, + _f: PhantomData<Rc<HookUd<'f>>>, +} + +/// 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. @@ -353,7 +395,7 @@ impl<F> InvalidContextError<F> { } } -impl<'ph> Drop for HookHandle<'ph> { +impl<'ph, 'f> Drop for HookHandle<'ph, 'f> where 'ph: 'f { fn drop(&mut self) { if self.freed.get() { // already free'd. @@ -361,7 +403,7 @@ impl<'ph> Drop for HookHandle<'ph> { } self.freed.set(true); unsafe { - let b = ((*self.ph.ph).hexchat_unhook)(self.ph.ph, self.hh) as *mut HookUd<'ph>; + 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)); @@ -369,6 +411,14 @@ impl<'ph> Drop for HookHandle<'ph> { } } +impl<'ph> Drop for PluginEntryHandle<'ph> { + fn drop(&mut self) { + unsafe { + ((*self.ph.ph).hexchat_plugingui_remove)(self.ph.ph, self.entry); + } + } +} + impl<'ph> Drop for Context<'ph> { fn drop(&mut self) { // check if we need to clean anything up @@ -410,9 +460,9 @@ impl<'ph> PluginHandle<'ph> { /// # Safety /// /// `ph` must be a valid pointer (see `std::ptr::read`). - unsafe fn new(ph: LtPhPtr<'ph>, info: PluginInfo, contexts: Contexts) -> PluginHandle<'ph> { + unsafe fn new(data: LtPhPtr<'ph>, info: PluginInfo, contexts: Contexts) -> PluginHandle<'ph> { PluginHandle { - ph, info, contexts + data, plugin: data.ph, info, contexts } } @@ -427,11 +477,12 @@ impl<'ph> PluginHandle<'ph> { /// ```no_run /// use hexchat_unsafe_plugin::PluginHandle; /// - /// fn init(ph: &mut 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() { @@ -453,6 +504,7 @@ impl<'ph> PluginHandle<'ph> { /// /// 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() { @@ -469,6 +521,7 @@ impl<'ph> PluginHandle<'ph> { /// /// 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() { @@ -485,6 +538,7 @@ impl<'ph> PluginHandle<'ph> { /// /// 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() { @@ -512,18 +566,42 @@ impl<'ph> PluginHandle<'ph> { } } + /// Operates on a virtual plugin. + pub fn for_entry_mut<F, R>(&mut self, entry: &mut PluginEntryHandle<'ph>, f: F) -> R where F: for<'eh> FnOnce(&mut PluginHandle<'eh>) -> R { + // we're doing something kinda (very) weird here, but this is the only + // way to get and set pluginprefs for virtual plugins. + // 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 [`set_context`] to + /// Note: The returned context may be invalid. Use [`Self::set_context`] to /// check. - /// - /// [`set_context`]: #method.set_context - pub fn get_context(&mut self) -> Context<'ph> { - let ctxp = unsafe { ((*self.ph.ph).hexchat_get_context)(self.ph.ph) }; + 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 { ((*self.ph.ph).hexchat_set_context)(self.ph.ph, ctxp) }; + let ok = unsafe { ph_call!(hexchat_set_context(self, ctxp)) }; unsafe { wrap_context(self, if ok == 0 { ptr::null() } else { ctxp }) } } @@ -533,7 +611,7 @@ impl<'ph> PluginHandle<'ph> { pub fn set_context(&mut self, ctx: &Context<'ph>) -> bool { if let Some(ctx) = ctx.ctx.upgrade() { unsafe { - ((*self.ph.ph).hexchat_set_context)(self.ph.ph, *ctx) != 0 + ph_call!(hexchat_set_context(self, *ctx)) != 0 } } else { false @@ -565,12 +643,16 @@ impl<'ph> PluginHandle<'ph> { /// 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<HookHandle<'ph>> { + /// fn register_commands<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph, 'ph>> { /// vec![ /// ph.hook_command("hello-world", hexchat_unsafe_plugin::PRI_NORM, Some("prints 'Hello, World!'"), |ph, arg, arg_eol| { /// ph.print("Hello, World!"); @@ -579,13 +661,14 @@ impl<'ph> PluginHandle<'ph> { /// ] /// } /// ``` - pub fn hook_command<F>(&mut self, cmd: &str, pri: i32, help: Option<&str>, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'ph + RefUnwindSafe { + 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, 'ph: 'f { + 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<HookUd> = rc_clone_from_raw(ud as *const HookUd); (f)(word, word_eol, ptr::null()).do_eat as c_int } let b: Rc<HookUd> = { - let ph = self.ph; + let ph = self.data; let info = self.info; let contexts = Rc::clone(&self.contexts); Rc::new(Box::new(move |word, word_eol, _| { @@ -605,9 +688,9 @@ impl<'ph> PluginHandle<'ph> { let help_text = help.map(CString::new).map(Result::unwrap); let bp = Rc::into_raw(b); unsafe { - let res = ((*self.ph.ph).hexchat_hook_command)(self.ph.ph, name.as_ptr(), pri as c_int, callback, help_text.as_ref().map(|s| s.as_ptr()).unwrap_or(ptr::null()), bp as *mut _); + 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.ph, hh: res, freed: Default::default(), _f: PhantomData } + HookHandle { ph: self.data, hh: res, freed: Default::default(), _f: PhantomData } } } /// Sets a server hook. @@ -617,7 +700,7 @@ impl<'ph> PluginHandle<'ph> { /// ```no_run /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle}; /// - /// fn register_server_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph>> { + /// fn register_server_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph, 'ph>> { /// vec![ /// ph.hook_server("PRIVMSG", hexchat_unsafe_plugin::PRI_NORM, |ph, word, word_eol| { /// if word.len() > 0 && word[0].starts_with('@') { @@ -628,17 +711,19 @@ impl<'ph> PluginHandle<'ph> { /// ] /// } /// ``` - pub fn hook_server<F>(&mut self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'ph + RefUnwindSafe { + 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, 'ph: 'f { + 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>(&mut self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol, EventAttrs) -> Eat + 'ph + RefUnwindSafe { + 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, 'ph: 'f { + 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<HookUd> = rc_clone_from_raw(ud as *const HookUd); (f)(word, word_eol, attrs).do_eat as c_int } let b: Rc<HookUd> = { - let ph = self.ph; + let ph = self.data; let info = self.info; let contexts = Rc::clone(&self.contexts); Rc::new(Box::new(move |word, word_eol, attrs| { @@ -658,9 +743,9 @@ impl<'ph> PluginHandle<'ph> { let name = CString::new(cmd).unwrap(); let bp = Rc::into_raw(b); unsafe { - let res = ((*self.ph.ph).hexchat_hook_server_attrs)(self.ph.ph, name.as_ptr(), pri as c_int, callback, bp as *mut _); + 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.ph, hh: res, freed: Default::default(), _f: PhantomData } + HookHandle { ph: self.data, hh: res, freed: Default::default(), _f: PhantomData } } } /// Sets a print hook. @@ -670,7 +755,7 @@ impl<'ph> PluginHandle<'ph> { /// ```no_run /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle}; /// - /// fn register_print_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph>> { + /// fn register_print_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph, 'ph>> { /// vec![ /// ph.hook_print("Channel Message", hexchat_unsafe_plugin::PRI_NORM, |ph, arg| { /// if let Some(nick) = arg.get(0) { @@ -683,7 +768,8 @@ impl<'ph> PluginHandle<'ph> { /// ] /// } /// ``` - pub fn hook_print<F>(&mut self, name: &str, pri: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word) -> Eat + 'ph + RefUnwindSafe { + 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, 'ph: 'f { + 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. :/ @@ -692,7 +778,7 @@ impl<'ph> PluginHandle<'ph> { (f)(word, ptr::null(), ptr::null()).do_eat as c_int } let b: Rc<HookUd> = { - let ph = self.ph; + let ph = self.data; let info = self.info; let contexts = Rc::clone(&self.contexts); Rc::new(Box::new(move |word, _, _| { @@ -710,9 +796,9 @@ impl<'ph> PluginHandle<'ph> { let name = CString::new(name).unwrap(); let bp = Rc::into_raw(b); unsafe { - let res = ((*self.ph.ph).hexchat_hook_print)(self.ph.ph, name.as_ptr(), pri as c_int, callback, bp as *mut _); + 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.ph, hh: res, freed: Default::default(), _f: PhantomData } + HookHandle { ph: self.data, hh: res, freed: Default::default(), _f: PhantomData } } } /// Sets a print hook, with attributes. @@ -722,7 +808,7 @@ impl<'ph> PluginHandle<'ph> { /// ```no_run /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle}; /// - /// fn register_print_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph>> { + /// fn register_print_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph, 'ph>> { /// vec![ /// ph.hook_print_attrs("Channel Message", hexchat_unsafe_plugin::PRI_NORM, |ph, arg, attrs| { /// if let Some(nick) = arg.get(0) { @@ -735,13 +821,14 @@ impl<'ph> PluginHandle<'ph> { /// ] /// } /// ``` - pub fn hook_print_attrs<F>(&mut self, name: &str, pri: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word, EventAttrs) -> Eat + 'ph + RefUnwindSafe { + 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, 'ph: 'f { + 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<HookUd> = rc_clone_from_raw(ud as *const HookUd); (f)(word, ptr::null(), attrs).do_eat as c_int } let b: Rc<HookUd> = { - let ph = self.ph; + let ph = self.data; let info = self.info; let contexts = Rc::clone(&self.contexts); Rc::new(Box::new(move |word, _, attrs| { @@ -760,9 +847,9 @@ impl<'ph> PluginHandle<'ph> { let name = CString::new(name).unwrap(); let bp = Rc::into_raw(b); unsafe { - let res = ((*self.ph.ph).hexchat_hook_print_attrs)(self.ph.ph, name.as_ptr(), pri as c_int, callback, bp as *mut _); + 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.ph, hh: res, freed: Default::default(), _f: PhantomData } + HookHandle { ph: self.data, hh: res, freed: Default::default(), _f: PhantomData } } } /// Sets a timer hook @@ -772,7 +859,7 @@ impl<'ph> PluginHandle<'ph> { /// ```no_run /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle}; /// - /// fn register_timers<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph>> { + /// fn register_timers<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph, 'ph>> { /// vec![ /// ph.hook_timer(2000, |ph| { /// ph.print("timer up!"); @@ -781,7 +868,8 @@ impl<'ph> PluginHandle<'ph> { /// ] /// } /// ``` - pub fn hook_timer<F>(&mut self, timeout: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>) -> bool + 'ph + RefUnwindSafe { + pub fn hook_timer<'f, F>(&self, timeout: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>) -> bool + 'f + RefUnwindSafe, 'ph: 'f { + 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<HookUd> = rc_clone_from_raw(ud as *const HookUd); (f)(ptr::null(), ptr::null(), ptr::null()).do_eat as c_int @@ -790,7 +878,7 @@ impl<'ph> PluginHandle<'ph> { // helps us clean up the thing when returning false let dropper = Rc::new(Cell::new(None)); let b: Rc<HookUd> = { - let ph = self.ph; + let ph = self.data; let info = self.info; let contexts = Rc::clone(&self.contexts); let freed = AssertUnwindSafe(Rc::clone(&freed)); @@ -828,18 +916,19 @@ impl<'ph> PluginHandle<'ph> { let bp = Rc::into_raw(b); dropper.set(Some(bp)); unsafe { - let res = ((*self.ph.ph).hexchat_hook_timer)(self.ph.ph, timeout as c_int, callback, bp as *mut _); + let res = ph_call!(hexchat_hook_timer(self, timeout as c_int, callback, bp as *mut _)); assert!(!res.is_null()); - HookHandle { ph: self.ph, hh: res, freed: freed, _f: PhantomData } + 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<T: ToString>(&mut self, s: T) { + // this checks the context internally. if it didn't, it wouldn't be safe to + // have here. + pub fn print<T: ToString>(&self, s: T) { let s = s.to_string(); unsafe { - hexchat_print_str(self.ph.ph, &*s, true); + hexchat_print_str(self.data.ph, &*s, true); } } @@ -851,47 +940,189 @@ impl<'ph> PluginHandle<'ph> { /// /// This panics if any broken formatting trait implementations are used in /// the format arguments. See also [`format!`]. - pub fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) { - self.print(fmt); + /// + /// # 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 information on the current context. - pub fn get_info<'a>(&'a mut self, id: InfoId) -> Option<&'a str> { - let ph = self.ph; + /// 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<Cow<'a, str>> { let id_cstring = CString::new(&*id.name()).unwrap(); unsafe { - let res = ((*ph.ph).hexchat_get_info)(ph.ph, id_cstring.as_ptr()); + 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(); - Some(s.expect("non-utf8 word - broken hexchat")) + // 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<CString> { + 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() } // ******* // // PRIVATE // // ******* // - fn find_valid_context(&mut self) -> Option<Context<'ph>> { + fn find_valid_context(&self) -> Option<Context<'ph>> { unsafe { - let ph = self.ph; + 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.ph).hexchat_list_get)(ph.ph, cstr(b"channels\0")); + 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 - if ((*ph.ph).hexchat_list_next)(ph.ph, list) != 0 { - // if this panics we may leak some memory. it's not a big deal tho, as it indicates - // a bug in hexchat-plugin.rs. - let ctx = ((*ph.ph).hexchat_list_str)(ph.ph, list, cstr(b"context\0")) as *const internals::HexchatContext; - ((*ph.ph).hexchat_list_free)(ph.ph, list); - Some(wrap_context(self, ctx)) + // 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 { - ((*ph.ph).hexchat_list_free)(ph.ph, list); None - } + }; + ph_call!(hexchat_list_free(self, list)); + ctx.map(|ctx| wrap_context(self, ctx)) } } } @@ -939,15 +1170,14 @@ impl<'a, 'ph: 'a> ValidContext<'a, 'ph> { */ /// Finds an open context for the given servname and channel. - pub fn find_context(&mut self, servname: Option<&str>, channel: Option<&str>) -> Option<Context<'ph>> { - // this was a mistake but oh well - let ph = self.ph.ph; + pub fn find_context(&self, servname: Option<&str>, channel: Option<&str>) -> Option<Context<'ph>> { + 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.ph).hexchat_find_context)(ph.ph, sptr, cptr) + ph_call!(hexchat_find_context(ph, sptr, cptr)) }; if ctx.is_null() { None @@ -957,15 +1187,14 @@ impl<'a, 'ph: 'a> ValidContext<'a, 'ph> { } /// Compares two nicks based on the server's case mapping. - pub fn nickcmp(&mut self, nick1: &str, nick2: &str) -> ::std::cmp::Ordering { + pub fn nickcmp(&self, nick1: &str, nick2: &str) -> ::std::cmp::Ordering { use std::cmp::Ordering; - // this was a mistake but oh well - let ph = self.ph.ph; + 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.ph).hexchat_nickcmp)(ph.ph, nick1.as_ptr(), nick2.as_ptr()) + ph_call!(hexchat_nickcmp(ph, nick1.as_ptr(), nick2.as_ptr())) }; if res < 0 { Ordering::Less @@ -977,8 +1206,7 @@ impl<'a, 'ph: 'a> ValidContext<'a, 'ph> { } pub fn send_modes<'b, I: IntoIterator<Item=&'b str>>(&mut self, iter: I, mpl: i32, sign: char, mode: char) { - // this was a mistake but oh well - let ph = self.ph.ph; + 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"); @@ -986,24 +1214,23 @@ impl<'a, 'ph: 'a> ValidContext<'a, 'ph> { 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.ph).hexchat_send_modes)(ph.ph, arr.as_mut_ptr(), arr.len() as c_int, - mpl as c_int, sign as c_char, mode as c_char) + 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)) } } /// Executes a command. pub fn command(self, cmd: &str) { - // this was a mistake but oh well - let ph = self.ph.ph; + let ph = self.ph; // need to put this in a more permanent position than temporaries let cmd = CString::new(cmd).unwrap(); unsafe { - ((*ph.ph).hexchat_command)(ph.ph, cmd.as_ptr()) + ph_call!(hexchat_command(ph, cmd.as_ptr())) } } pub fn emit_print<'b, I: IntoIterator<Item=&'b str>>(self, event: &str, args: I) -> bool { - let ph = self.ph.ph; + let ph = self.ph; let event = CString::new(event).unwrap(); let mut args_cs: [Option<CString>; 4] = [None, None, None, None]; { @@ -1024,12 +1251,12 @@ impl<'a, 'ph: 'a> ValidContext<'a, 'ph> { argv[i] = args_cs[i].as_ref().map_or(ptr::null(), |s| s.as_ptr()); } unsafe { - ((*ph.ph).hexchat_emit_print)(ph.ph, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4]) != 0 + ph_call!(hexchat_emit_print(ph, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4])) != 0 } } pub fn emit_print_attrs<'b, I: IntoIterator<Item=&'b str>>(self, attrs: EventAttrs, event: &str, args: I) -> bool { - let ph = self.ph.ph; + let ph = self.ph; let event = CString::new(event).unwrap(); let mut args_cs: [Option<CString>; 4] = [None, None, None, None]; { @@ -1049,9 +1276,9 @@ impl<'a, 'ph: 'a> ValidContext<'a, 'ph> { 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.ph, attrs) }; + let helper = unsafe { HexchatEventAttrsHelper::new_with(ph, attrs) }; unsafe { - ((*ph.ph).hexchat_emit_print_attrs)(ph.ph, helper.0, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4]) != 0 + ph_call!(hexchat_emit_print_attrs(ph, helper.0, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4])) != 0 } } @@ -1061,62 +1288,102 @@ impl<'a, 'ph: 'a> ValidContext<'a, 'ph> { // We can't just deref because then you could recursively ensure valid context and then it'd no // longer work. - pub fn get_context(&mut self) -> Context<'ph> { + /// 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(ctx) } /// Prints to the hexchat buffer. - // as of hexchat 2.14.1, this does not call hooks. - pub fn print<T: ToString>(&mut self, s: T) { + /// + /// See [`PluginHandle::print`]. + pub fn print<T: ToString>(&self, s: T) { self.ph.print(s) } - /// Prints to this hexchat buffer. + /// 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(&mut self, fmt: fmt::Arguments<'_>) { + pub fn write_fmt(&self, fmt: fmt::Arguments<'_>) { self.ph.write_fmt(fmt) } - /// Sets a command hook - pub fn hook_command<F>(&mut self, cmd: &str, pri: i32, help: Option<&str>, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'ph + RefUnwindSafe { + /// 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, 'ph: 'f { self.ph.hook_command(cmd, pri, help, cb) } - /// Sets a server hook - pub fn hook_server<F>(&mut self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'ph + RefUnwindSafe { + /// 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, 'ph: 'f { self.ph.hook_server(cmd, pri, cb) } - /// Sets a server hook with attributes - pub fn hook_server_attrs<F>(&mut self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol, EventAttrs) -> Eat + 'ph + RefUnwindSafe { + /// 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, 'ph: 'f { self.ph.hook_server_attrs(cmd, pri, cb) } - /// Sets a print hook - pub fn hook_print<F>(&mut self, name: &str, pri: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word) -> Eat + 'ph + RefUnwindSafe { + /// 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, 'ph: 'f { self.ph.hook_print(name, pri, cb) } - /// Sets a print hook with attributes - pub fn hook_print_attrs<F>(&mut self, name: &str, pri: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word, EventAttrs) -> Eat + 'ph + RefUnwindSafe { + /// 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, 'ph: 'f { self.ph.hook_print_attrs(name, pri, cb) } - /// Sets a timer hook - pub fn hook_timer<F>(&mut self, timeout: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>) -> bool + 'ph + RefUnwindSafe { + /// 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, 'ph: 'f { self.ph.hook_timer(timeout, cb) } - pub fn get_info<'b>(&'b mut self, id: InfoId) -> Option<&'b str> { + /// Returns context and client information. + /// + /// See [`PluginHandle::get_info`]. + pub fn get_info<'b>(&'b self, id: InfoId) -> Option<Cow<'b, str>> { self.ph.get_info(id) } + /// Returns the plugin directory. + /// + /// See [`PluginHandle::get_libdirfs`]. + pub fn get_libdirfs(&self) -> Option<CString> { + 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) + } } // ******* // @@ -1138,7 +1405,7 @@ impl<'a, 'ph: 'a> ValidContext<'a, 'ph> { // /// Userdata type used by a timer hook. // type TimerHookUd = Box<dyn Fn() -> bool + ::std::panic::RefUnwindSafe>; /// Userdata type used by a hook -type HookUd<'ph> = Box<dyn Fn(*const *const c_char, *const *const c_char, *const RawAttrs) -> Eat + RefUnwindSafe + 'ph>; +type HookUd<'f> = Box<dyn Fn(*const *const c_char, *const *const c_char, *const RawAttrs) -> Eat + RefUnwindSafe + 'f>; /// Contexts type Contexts = Rc<AssertUnwindSafe<RefCell<HashSet<Rc<*const internals::HexchatContext>>>>>; @@ -1173,7 +1440,7 @@ unsafe fn rc_clone_from_raw<T>(ptr: *const T) -> Rc<T> { /// # Safety /// /// This function doesn't validate the context. -unsafe fn wrap_context<'ph>(ph: &mut PluginHandle<'ph>, ctx: *const internals::HexchatContext) -> Context<'ph> { +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 } @@ -1213,16 +1480,16 @@ unsafe fn hexchat_print_str(ph: *mut RawPh, s: &str, panic_on_nul: bool) { } /// Helper to manage owned RawAttrs -struct HexchatEventAttrsHelper(*mut RawAttrs, *mut RawPh); +struct HexchatEventAttrsHelper<'a, 'ph>(*mut RawAttrs, &'a PluginHandle<'ph>) where 'ph: 'a; -impl HexchatEventAttrsHelper { +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: *mut RawPh) -> Self { - HexchatEventAttrsHelper(((*ph).hexchat_event_attrs_create)(ph), ph) + 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`. @@ -1230,7 +1497,7 @@ impl HexchatEventAttrsHelper { /// # Safety /// /// `ph` must be a valid raw plugin handle. - unsafe fn new_with(ph: *mut RawPh, attrs: EventAttrs<'_>) -> Self { + unsafe fn new_with(ph: &'a PluginHandle<'ph>, attrs: EventAttrs<'_>) -> Self { let helper = Self::new(ph); let v = attrs.server_time.or(Some(UNIX_EPOCH)).map(|st| match st.duration_since(UNIX_EPOCH) { Ok(n) => n.as_secs(), @@ -1241,20 +1508,20 @@ impl HexchatEventAttrsHelper { } } -impl Drop for HexchatEventAttrsHelper { +impl<'a, 'ph> Drop for HexchatEventAttrsHelper<'a, 'ph> where 'ph: 'a { fn drop(&mut self) { unsafe { - ((*self.1).hexchat_event_attrs_free)(self.1, self.0) + ph_call!(hexchat_event_attrs_free(self.1, self.0)) } } } /// Plugin data stored in the hexchat plugin_handle. struct PhUserdata<'ph> { - plug: Box<dyn Plugin<'ph> + 'ph>, + plug: Pin<Box<dyn Plugin<'ph> + 'ph>>, contexts: Contexts, // this is never read, but we need to not drop it until we can drop it - _context_hook: HookHandle<'ph>, + _context_hook: HookHandle<'ph, 'ph>, pluginfo: PluginInfo, } @@ -1265,31 +1532,17 @@ struct PhUserdata<'ph> { /// 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: Rc<PhUserdata<'ph>>) { - (*ph.ph).userdata = Rc::into_raw(ud) as *mut c_void; +unsafe fn put_userdata<'ph>(ph: LtPhPtr<'ph>, ud: Box<PhUserdata<'ph>>) { + (*ph.ph).userdata = Box::into_raw(ud) as *mut c_void; } -// /// Clones the userdata from the plugin handle. -// /// -// /// # Safety -// /// -// /// This function is unsafe because it doesn't check if the pointer is valid. -// unsafe fn clone_userdata(ph: *mut RawPh) -> Option<Rc<PhUserdata>> { -// let ptr = (*ph).userdata as *const PhUserdata; -// if ptr.is_null() { -// None -// } else { -// Some(rc_clone_from_raw(ptr)) -// } -// } - /// 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>) -> Rc<PhUserdata<'ph>> { - Rc::from_raw(mem::replace(&mut (*ph.ph).userdata, ptr::null_mut()) as *mut PhUserdata<'ph>) +unsafe fn pop_userdata<'ph>(ph: LtPhPtr<'ph>) -> Box<PhUserdata<'ph>> { + Box::from_raw(mem::replace(&mut (*ph.ph).userdata, ptr::null_mut()) as *mut PhUserdata<'ph>) } // *********************** // @@ -1355,7 +1608,7 @@ pub unsafe fn hexchat_plugin_init<'ph, T>(plugin_handle: LtPhPtr<'ph>, } else { return 0; }; - let r: thread::Result<Option<Rc<_>>> = { + let r: thread::Result<Option<Box<_>>> = { catch_unwind(move || { // AssertUnwindSafe not Default at the time of writing this let contexts = Rc::new(AssertUnwindSafe(Default::default())); @@ -1364,15 +1617,15 @@ pub unsafe fn hexchat_plugin_init<'ph, T>(plugin_handle: LtPhPtr<'ph>, // 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 = unsafe { ((*ph.ph.ph).hexchat_get_context)(ph.ph.ph) }; + let ctx = unsafe { ph_call!(hexchat_get_context(ph)) }; contexts.borrow_mut().remove(&ctx); EAT_NONE }); let contexts = Rc::clone(&pluginhandle.contexts); - let plug = T::default(); - if plug.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 }) { + 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(Rc::new(PhUserdata { plug: Box::new(plug), pluginfo, contexts, _context_hook: context_hook })) + Some(Box::new(PhUserdata { plug, pluginfo, contexts, _context_hook: context_hook })) } else { // TODO log: forgot to call register None @@ -1412,9 +1665,9 @@ pub unsafe fn hexchat_plugin_deinit<'ph, T>(plugin_handle: LtPhPtr<'ph>) -> c_in { let mut ausinfo = AssertUnwindSafe(&mut info); safe_to_unload = if catch_unwind(move || { - let userdata = pop_userdata(plugin_handle); + let mut userdata = pop_userdata(plugin_handle); let pluginfo = userdata.pluginfo; - userdata.plug.deinit(&mut PluginHandle::new(plugin_handle, pluginfo, Rc::clone(&userdata.contexts))); + userdata.plug.as_mut().deinit(&mut PluginHandle::new(plugin_handle, pluginfo, Rc::clone(&userdata.contexts))); drop(userdata); **ausinfo = Some(pluginfo); // we drop the plugin regardless of whether or not diff --git a/src/strip.rs b/src/strip.rs new file mode 100644 index 0000000..b10fb44 --- /dev/null +++ b/src/strip.rs @@ -0,0 +1,134 @@ +// This file is part of Hexchat Plugin API Bindings for Rust +// Copyright (C) 2022 Soni L. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +/// Stripping mode for [`PluginHandle::strip`](crate::PluginHandle::strip). +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)] +pub struct Strip { + // 4 + hidden: bool, + // 2 + formatting: bool, + // 1 + colors: bool, +} + +impl Strip { + /// Creates a new `Strip` that, by default, strips no attributes. + /// + /// # Examples + /// + /// ```no_run + /// use hexchat_unsafe_plugin::{PluginHandle, Strip}; + /// + /// fn strip_nothing(s: &str, ph: &PluginHandle<'_>) -> String { + /// ph.strip(s, Strip::new()) + /// } + /// ``` + pub const fn new() -> Strip { + Strip { + hidden: false, + formatting: false, + colors: false, + } + } + + /// Sets whether to remove mIRC color attributes. + /// + /// # Examples + /// + /// ```no_run + /// use hexchat_unsafe_plugin::{PluginHandle, Strip}; + /// + /// fn strip_colors(s: &str, ph: &PluginHandle<'_>) -> String { + /// ph.strip(s, Strip::new().colors(true)) + /// } + /// ``` + pub const fn colors(mut self, strip: bool) -> Self { + self.colors = strip; + self + } + + /// Sets whether to remove formatting attributes. + /// + /// # Examples + /// + /// ```no_run + /// use hexchat_unsafe_plugin::{PluginHandle, Strip}; + /// + /// fn strip_formatting(s: &str, ph: &PluginHandle<'_>) -> String { + /// ph.strip(s, Strip::new().formatting(true)) + /// } + /// ``` + pub const fn formatting(mut self, strip: bool) -> Self { + self.formatting = strip; + self + } + + /// Sets whether to remove internal "hidden text" formatting attributes. + /// + /// This is split from [`Self::formatting`] because these attributes are + /// only processed when writing directly to a buffer - they're for + /// internal/plugin use. This tends to be useful when processing user or + /// remote input and writing it directly to a buffer. + /// + /// # Examples + /// + /// ```no_run + /// use hexchat_unsafe_plugin::{PluginHandle, Strip}; + /// + /// fn strip_hidden(s: &str, ph: &PluginHandle<'_>) -> String { + /// ph.strip(s, Strip::new().hidden(true)) + /// } + /// ``` + pub const fn hidden(mut self, strip: bool) -> Self { + self.hidden = strip; + self + } + + /// Creates a new `Strip` that strips all strippable attributes. + /// + /// # Examples + /// + /// ```no_run + /// use hexchat_unsafe_plugin::{PluginHandle, Strip}; + /// + /// fn strip_all(s: &str, ph: &PluginHandle<'_>) -> String { + /// ph.strip(s, Strip::all()) + /// } + /// ``` + pub const fn all() -> Strip { + Strip { + hidden: true, + formatting: true, + colors: true, + } + } + + /// Builds the flags for FFI. + pub(crate) fn flags(self) -> ::libc::c_int { + let mut value = 0; + if self.hidden { + value |= 4; + } + if self.formatting { + value |= 2; + } + if self.colors { + value |= 1; + } + value + } +} |