diff options
author | SoniEx2 <endermoneymod@gmail.com> | 2022-04-11 19:33:04 -0300 |
---|---|---|
committer | SoniEx2 <endermoneymod@gmail.com> | 2022-04-11 22:39:16 -0300 |
commit | 207026ae5e35da096b8110afe1ba2973a0e54fdb (patch) | |
tree | 2612bfe820324034716409a27c5317efa39ba9a4 | |
parent | 3b38f6e4f418baafe48989a90ce17661a6c2966f (diff) |
[Project] hexchat-unsafe-plugin.rs
A best-effort safe wrapper for the hexchat C API. Enables writing native hexchat plugins in mostly-safe Rust.
-rw-r--r-- | Cargo.toml | 8 | ||||
-rw-r--r-- | README.md | 22 | ||||
-rw-r--r-- | src/lib.rs | 592 | ||||
-rw-r--r-- | src/word.rs | 14 |
4 files changed, 383 insertions, 253 deletions
diff --git a/Cargo.toml b/Cargo.toml index bab4aa6..a983dad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "hexchat-plugin" -version = "0.2.14" +name = "hexchat-unsafe-plugin" +version = "1.0.0" authors = ["SoniEx2 <endermoneymod@gmail.com>"] -description = "Lets you write HexChat plugins in Rust" -license = "AGPL-3.0+" +description = "Lets you write native HexChat plugins in mostly-safe Rust" +license = "GPL-3.0-or-later" repository = "https://soniex2.autistic.space/git-repos/hexchat-plugin.git" homepage = "https://ganarchy.autistic.space/project/7597bdc9d42badc332ecd57ab81e71c41fa2b595" keywords = ["hexchat", "plugin", "hexchat-plugin"] diff --git a/README.md b/README.md index 9effee2..26c452b 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -Rust hexchat-plugin -=================== +Rust hexchat-unsafe-plugin +========================== -`hexchat-plugin` provides safe and rusty API bindings for developing native +`hexchat-unsafe-plugin` provides rusty API bindings for developing native HexChat plugins. To get started, implement `hexchat_plugin::Plugin` on a struct and use the -`hexchat_plugin!(impl Plugin)` macro in your `lib.rs`. Do not provide a `main`, -as it will not work. Make sure to use the `cdylib` crate-type, or it will also -not work. +`hexchat_plugin!('a, impl Plugin<'a>)` macro in your `lib.rs`. Do not provide a +`main`, as it will not work. Make sure to use the `cdylib` crate-type, or it +will also not work. Also note that plugins may be loaded multiple times. It's recommended to avoid using global state (statics, thread locals) so as to not cause issues with it. @@ -16,25 +16,25 @@ Examples -------- For a production-grade plugin using this crate, take a look at -<https://github.com/SoniEx2/cw2-hexchat>. +[TODO]. License ------- ```text Hexchat Plugin API Bindings for Rust -Copyright (C) 2018, 2021 Soni L. +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 Affero General Public License as +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 Affero General Public License for more details. +GNU General Public License for more details. -You should have received a copy of the GNU Affero General Public License +You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. ``` diff --git a/src/lib.rs b/src/lib.rs index a4faec7..4dc1bfe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ // Hexchat Plugin API Bindings for Rust -// Copyright (C) 2018, 2021 Soni L. +// 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 Affero General Public License as @@ -32,28 +32,28 @@ //! //! ```no_run //! #[macro_use] -//! extern crate hexchat_plugin; +//! extern crate hexchat_unsafe_plugin; //! //! use std::sync::Mutex; -//! use hexchat_plugin::{Plugin, PluginHandle, HookHandle}; +//! use hexchat_unsafe_plugin::{Plugin, PluginHandle, HookHandle}; //! //! #[derive(Default)] -//! struct MyPlugin { -//! cmd: Mutex<Option<HookHandle>> +//! struct MyPlugin<'ph> { +//! cmd: Mutex<Option<HookHandle<'ph>>> //! } //! -//! impl Plugin for MyPlugin { -//! fn init(&self, ph: &mut PluginHandle, arg: Option<&str>) -> bool { +//! unsafe impl<'ph> Plugin<'ph> for MyPlugin<'ph> { +//! fn init(&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", |ph, arg, arg_eol| { +//! *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_plugin::EAT_ALL -//! }, hexchat_plugin::PRI_NORM, Some("prints 'Hello, World!'"))); +//! hexchat_unsafe_plugin::EAT_ALL +//! })); //! true //! } //! } //! -//! hexchat_plugin!(MyPlugin); +//! hexchat_plugin!('ph, MyPlugin<'ph>); //! //! # fn main() { } // satisfy the compiler, we can't actually run the code //! ``` @@ -62,40 +62,52 @@ * 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. + * - 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`). + * 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. + * `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_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_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_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. * - * `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. */ /* @@ -111,7 +123,7 @@ * -[ ] Finish API support. [PRI-HIGH] * -[x] word * -[x] word_eol - * -[#] HEXCHAT_PRI_{HIGHEST, HIGH, NORM, LOW, LOWEST} + * -[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.) @@ -174,7 +186,10 @@ use internals::HexchatEventAttrs as RawAttrs; use std::borrow::Cow; use std::cell::Cell; +use std::cell::RefCell; +use std::collections::HashSet; use std::ffi::{CString, CStr}; +use std::fmt; use std::marker::PhantomData; use std::mem; use std::mem::ManuallyDrop; @@ -210,46 +225,92 @@ pub const PRI_LOWEST: i32 = -128; // Traits /// A hexchat plugin. -pub trait 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. +pub unsafe trait Plugin<'ph> { /// Called to initialize the plugin. - fn init(&self, ph: &mut PluginHandle, arg: Option<&str>) -> bool; + fn init(&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) { + fn deinit(&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`. +#[repr(transparent)] +#[doc(hidden)] +#[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>, +} + /// A hexchat plugin handle, used to register hooks and interact with hexchat. /// /// # Examples /// /// ```no_run -/// use hexchat_plugin::{PluginHandle}; +/// use hexchat_unsafe_plugin::{PluginHandle}; /// /// fn init(ph: &mut PluginHandle) { /// ph.register("myplug", "0.1.0", "my awesome plug"); /// } /// ``` -pub struct PluginHandle { - ph: *mut RawPh, - skip_pri_ck: bool, +pub struct PluginHandle<'ph> { + ph: LtPhPtr<'ph>, + contexts: Contexts, // Used for registration info: PluginInfo, } -/// A safety wrapper to ensure you're working with a valid context. -/// -/// This mechanism attempts to reduce the likelihood of segfaults. -pub struct EnsureValidContext<'a> { - ph: &'a mut PluginHandle, +mod valid_context { + use crate::PluginHandle; + + /// A PluginHandle operating on a valid context. + /// + /// This mechanism attempts to reduce the likelihood of segfaults in + /// hexchat code. + /// + /// See also [`PluginHandle::ensure_valid_context`]. + pub struct ValidContext<'a, 'ph: 'a> { + pub(crate) ph: &'a mut PluginHandle<'ph>, + _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 { + Self { ph, _hidden: () } + } + } } +pub use valid_context::ValidContext; /// Event attributes. +// TODO better docs. #[derive(Clone)] pub struct EventAttrs<'a> { /// Server time. @@ -257,25 +318,28 @@ pub struct EventAttrs<'a> { _dummy: PhantomData<&'a ()>, } -/// A hook handle. +/// A hook handle, used to enable unhooking. #[must_use = "Hooks must be stored somewhere and are automatically unhooked on Drop"] -pub struct HookHandle { - ph: *mut RawPh, +pub struct HookHandle<'ph> { + 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>>, + _f: PhantomData<Rc<HookUd<'ph>>>, } /// A context. #[derive(Clone)] -pub struct Context { - ctx: RcWeak<*const internals::HexchatContext>, // may be null - closure: Rc<Cell<Option<HookHandle>>>, +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: FnOnce(EnsureValidContext) -> R, R>(F); +pub struct InvalidContextError<F>(F); // Enums @@ -283,12 +347,13 @@ pub struct InvalidContextError<F: FnOnce(EnsureValidContext) -> R, R>(F); // impls // // ***** // -impl<F: FnOnce(EnsureValidContext) -> R, R> InvalidContextError<F, R> { +impl<F> InvalidContextError<F> { pub fn get_closure(self) -> F { self.0 } } -impl Drop for HookHandle { + +impl<'ph> Drop for HookHandle<'ph> { fn drop(&mut self) { if self.freed.get() { // already free'd. @@ -296,7 +361,7 @@ impl Drop for HookHandle { } self.freed.set(true); unsafe { - let b = ((*self.ph).hexchat_unhook)(self.ph, self.hh) as *mut HookUd; + let b = ((*self.ph.ph).hexchat_unhook)(self.ph.ph, self.hh) as *mut HookUd<'ph>; // we assume b is not null. this should be safe. // just drop it drop(Rc::from_raw(b)); @@ -304,6 +369,16 @@ impl Drop for HookHandle { } } +impl<'ph> Drop for Context<'ph> { + 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); + } + } +} + /// Handles a hook panic at the C-Rust ABI boundary. /// /// # Safety @@ -329,15 +404,15 @@ unsafe fn call_hook_protected<F: FnOnce() -> Eat + UnwindSafe>( } } -impl PluginHandle { +impl<'ph> PluginHandle<'ph> { /// Wraps the raw handle. /// /// # Safety /// /// `ph` must be a valid pointer (see `std::ptr::read`). - unsafe fn new(ph: *mut RawPh, info: PluginInfo) -> PluginHandle { + unsafe fn new(ph: LtPhPtr<'ph>, info: PluginInfo, contexts: Contexts) -> PluginHandle<'ph> { PluginHandle { - ph, info, skip_pri_ck: false, + ph, info, contexts } } @@ -350,7 +425,7 @@ impl PluginHandle { /// # Examples /// /// ```no_run - /// use hexchat_plugin::PluginHandle; + /// use hexchat_unsafe_plugin::PluginHandle; /// /// fn init(ph: &mut PluginHandle) { /// ph.register("foo", "0.1.0", "my foo plugin"); @@ -425,11 +500,8 @@ impl PluginHandle { /// # Panics /// /// This function may panic if it's called while hexchat is closing. - // NOTE: using a closure is nicer. - // TODO check if this is actually safe - pub fn ensure_valid_context<F, R>(&mut self, f: F) -> R where F: FnOnce(EnsureValidContext) -> R { + pub fn ensure_valid_context<F, R>(&mut self, f: F) -> R where F: for<'a> FnOnce(ValidContext<'a, 'ph>) -> R { let ctx = self.get_context(); - // need this here because we don't have NLL yet let res = self.with_context(&ctx, f); match res { Result::Ok(r @ _) => r, @@ -442,24 +514,26 @@ impl PluginHandle { /// Returns the current context. /// - /// Note: The returned context may be invalid. Use [`set_context`] to check. + /// Note: The returned context may be invalid. Use [`set_context`] to + /// check. /// /// [`set_context`]: #method.set_context - pub fn get_context(&mut self) -> Context { - let ctxp = unsafe { ((*self.ph).hexchat_get_context)(self.ph) }; - // 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).hexchat_set_context)(self.ph, ctxp) }; + pub fn get_context(&mut self) -> Context<'ph> { + let ctxp = unsafe { ((*self.ph.ph).hexchat_get_context)(self.ph.ph) }; + // 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) }; 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) -> bool { + pub fn set_context(&mut self, ctx: &Context<'ph>) -> bool { if let Some(ctx) = ctx.ctx.upgrade() { unsafe { - ((*self.ph).hexchat_set_context)(self.ph, *ctx) != 0 + ((*self.ph.ph).hexchat_set_context)(self.ph.ph, *ctx) != 0 } } else { false @@ -468,23 +542,24 @@ impl PluginHandle { /// Do something in a valid context. /// - /// Note that this changes the active context and doesn't change it back. + /// 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, - /// calls `f` and returns `Ok(its result)`. + /// 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. + /// Note that `InvalidContextError` contains the original closure. This + /// allows you to retry, if you so wish. /// /// [`set_context`]: #method.set_context - // this is probably safe to inline, and actually a good idea for ensure_valid_context #[inline] - pub fn with_context<F, R>(&mut self, ctx: &Context, f: F) -> Result<R, InvalidContextError<F, R>> where F: FnOnce(EnsureValidContext) -> R { + pub fn with_context<F, R>(&mut self, ctx: &Context<'ph>, f: F) -> Result<R, InvalidContextError<F>> where F: for<'a> FnOnce(ValidContext<'a, 'ph>) -> R { if !self.set_context(ctx) { Err(InvalidContextError(f)) } else { - Ok(f(EnsureValidContext { ph: self })) + Ok(f(unsafe { ValidContext::new(self) })) } } @@ -493,18 +568,18 @@ impl PluginHandle { /// # Examples /// /// ```no_run - /// use hexchat_plugin::{PluginHandle, HookHandle}; + /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle}; /// - /// fn register_commands(ph: &mut PluginHandle) -> Vec<HookHandle> { + /// fn register_commands<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph>> { /// vec![ - /// ph.hook_command("hello-world", |ph, arg, arg_eol| { + /// ph.hook_command("hello-world", hexchat_unsafe_plugin::PRI_NORM, Some("prints 'Hello, World!'"), |ph, arg, arg_eol| { /// ph.print("Hello, World!"); - /// hexchat_plugin::EAT_ALL - /// }, hexchat_plugin::PRI_NORM, Some("prints 'Hello, World!'")), + /// hexchat_unsafe_plugin::EAT_ALL + /// }), /// ] /// } /// ``` - pub fn hook_command<F>(&mut self, cmd: &str, cb: F, pri: i32, help: Option<&str>) -> HookHandle where F: Fn(&mut PluginHandle, Word, WordEol) -> Eat + 'static + RefUnwindSafe { + 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 { 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 @@ -512,11 +587,13 @@ impl PluginHandle { let b: Rc<HookUd> = { let ph = self.ph; 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, move || { - let mut ph = PluginHandle::new(ph, info); + 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) @@ -528,7 +605,7 @@ impl PluginHandle { let help_text = help.map(CString::new).map(Result::unwrap); let bp = Rc::into_raw(b); unsafe { - let res = ((*self.ph).hexchat_hook_command)(self.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 = ((*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 _); assert!(!res.is_null()); HookHandle { ph: self.ph, hh: res, freed: Default::default(), _f: PhantomData } } @@ -538,24 +615,24 @@ impl PluginHandle { /// # Examples /// /// ```no_run - /// use hexchat_plugin::{PluginHandle, HookHandle}; + /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle}; /// - /// fn register_server_hooks(ph: &mut PluginHandle) -> Vec<HookHandle> { + /// fn register_server_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph>> { /// vec![ - /// ph.hook_server("PRIVMSG", |ph, word, word_eol| { + /// 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_plugin::EAT_NONE - /// }, hexchat_plugin::PRI_NORM), + /// hexchat_unsafe_plugin::EAT_NONE + /// }), /// ] /// } /// ``` - pub fn hook_server<F>(&mut self, cmd: &str, cb: F, pri: i32) -> HookHandle where F: Fn(&mut PluginHandle, Word, WordEol) -> Eat + 'static + RefUnwindSafe { - self.hook_server_attrs(cmd, move |ph, w, we, _| cb(ph, w, we), pri) + 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 { + 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, cb: F, pri: i32) -> HookHandle where F: Fn(&mut PluginHandle, Word, WordEol, EventAttrs) -> Eat + 'static + RefUnwindSafe { + 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 { 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 @@ -563,11 +640,13 @@ impl PluginHandle { let b: Rc<HookUd> = { let ph = self.ph; 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, move || { - let mut ph = PluginHandle::new(ph, info); + 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(); @@ -579,7 +658,7 @@ impl PluginHandle { let name = CString::new(cmd).unwrap(); let bp = Rc::into_raw(b); unsafe { - let res = ((*self.ph).hexchat_hook_server_attrs)(self.ph, name.as_ptr(), pri as c_int, callback, bp as *mut _); + let res = ((*self.ph.ph).hexchat_hook_server_attrs)(self.ph.ph, 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 } } @@ -589,22 +668,22 @@ impl PluginHandle { /// # Examples /// /// ```no_run - /// use hexchat_plugin::{PluginHandle, HookHandle}; + /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle}; /// - /// fn register_print_hooks(ph: &mut PluginHandle) -> Vec<HookHandle> { + /// fn register_print_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph>> { /// vec![ - /// ph.hook_print("Channel Message", |ph, arg| { + /// 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_plugin::EAT_ALL + /// return hexchat_unsafe_plugin::EAT_ALL /// } /// } - /// hexchat_plugin::EAT_NONE - /// }, hexchat_plugin::PRI_NORM), + /// hexchat_unsafe_plugin::EAT_NONE + /// }), /// ] /// } /// ``` - pub fn hook_print<F>(&mut self, name: &str, cb: F, mut pri: i32) -> HookHandle where F: Fn(&mut PluginHandle, Word) -> Eat + 'static + RefUnwindSafe { + 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 { // 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. :/ @@ -612,18 +691,16 @@ impl PluginHandle { let f: Rc<HookUd> = rc_clone_from_raw(ud as *const HookUd); (f)(word, ptr::null(), ptr::null()).do_eat as c_int } - if name == "Close Context" && (pri as c_int == c_int::min_value()) && !self.skip_pri_ck { - self.print("Warning: Attempted to hook Close Context with priority INT_MIN. Adjusting to INT_MIN+1."); - pri = (c_int::min_value() + 1) as i32; - } let b: Rc<HookUd> = { let ph = self.ph; 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, move || { - let mut ph = PluginHandle::new(ph, info); + call_hook_protected(ph.ph, move || { + let mut ph = PluginHandle::new(ph, info, contexts); let word = Word::new(word); cb(&mut ph, word) }) @@ -633,7 +710,7 @@ impl PluginHandle { let name = CString::new(name).unwrap(); let bp = Rc::into_raw(b); unsafe { - let res = ((*self.ph).hexchat_hook_print)(self.ph, name.as_ptr(), pri as c_int, callback, bp as *mut _); + let res = ((*self.ph.ph).hexchat_hook_print)(self.ph.ph, 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 } } @@ -643,22 +720,22 @@ impl PluginHandle { /// # Examples /// /// ```no_run - /// use hexchat_plugin::{PluginHandle, HookHandle}; + /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle}; /// - /// fn register_print_hooks(ph: &mut PluginHandle) -> Vec<HookHandle> { + /// fn register_print_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph>> { /// vec![ - /// ph.hook_print_attrs("Channel Message", |ph, arg, attrs| { + /// 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_plugin::EAT_ALL + /// return hexchat_unsafe_plugin::EAT_ALL /// } /// } - /// hexchat_plugin::EAT_NONE - /// }, hexchat_plugin::PRI_NORM), + /// hexchat_unsafe_plugin::EAT_NONE + /// }), /// ] /// } /// ``` - pub fn hook_print_attrs<F>(&mut self, name: &str, cb: F, pri: i32) -> HookHandle where F: Fn(&mut PluginHandle, Word, EventAttrs) -> Eat + 'static + RefUnwindSafe { + 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 { 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 @@ -666,11 +743,13 @@ impl PluginHandle { let b: Rc<HookUd> = { let ph = self.ph; 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, move || { - let mut ph = PluginHandle::new(ph, info); + 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) @@ -681,7 +760,7 @@ impl PluginHandle { let name = CString::new(name).unwrap(); let bp = Rc::into_raw(b); unsafe { - let res = ((*self.ph).hexchat_hook_print_attrs)(self.ph, name.as_ptr(), pri as c_int, callback, bp as *mut _); + let res = ((*self.ph.ph).hexchat_hook_print_attrs)(self.ph.ph, 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 } } @@ -691,9 +770,9 @@ impl PluginHandle { /// # Examples /// /// ```no_run - /// use hexchat_plugin::{PluginHandle, HookHandle}; + /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle}; /// - /// fn register_timers(ph: &mut PluginHandle) -> Vec<HookHandle> { + /// fn register_timers<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph>> { /// vec![ /// ph.hook_timer(2000, |ph| { /// ph.print("timer up!"); @@ -702,7 +781,7 @@ impl PluginHandle { /// ] /// } /// ``` - pub fn hook_timer<F>(&mut self, timeout: i32, cb: F) -> HookHandle where F: Fn(&mut PluginHandle) -> bool + 'static + RefUnwindSafe { + pub fn hook_timer<F>(&mut self, timeout: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>) -> bool + 'ph + RefUnwindSafe { 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 @@ -713,13 +792,15 @@ impl PluginHandle { let b: Rc<HookUd> = { let ph = self.ph; 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, move || { - let mut ph = PluginHandle::new(ph, info); + call_hook_protected(ph.ph, move || { + let mut ph = PluginHandle::new(ph, info, contexts); if cb(&mut ph) { EAT_HEXCHAT } else { @@ -735,7 +816,7 @@ impl PluginHandle { // 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, || { + call_hook_protected(ph.ph, || { drop(Rc::from_raw(dropper.take().unwrap())); EAT_NONE }); @@ -747,7 +828,7 @@ impl PluginHandle { let bp = Rc::into_raw(b); dropper.set(Some(bp)); unsafe { - let res = ((*self.ph).hexchat_hook_timer)(self.ph, timeout as c_int, callback, bp as *mut _); + let res = ((*self.ph.ph).hexchat_hook_timer)(self.ph.ph, timeout as c_int, callback, bp as *mut _); assert!(!res.is_null()); HookHandle { ph: self.ph, hh: res, freed: freed, _f: PhantomData } } @@ -755,20 +836,31 @@ impl PluginHandle { /// Prints to the hexchat buffer. // this checks the context internally. if it didn't, it wouldn't be safe to have here. - // TODO write_fmt instead. pub fn print<T: ToString>(&mut self, s: T) { let s = s.to_string(); unsafe { - hexchat_print_str(self.ph, &*s, true); + hexchat_print_str(self.ph.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!`]. + pub fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) { + 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; let id_cstring = CString::new(&*id.name()).unwrap(); unsafe { - let res = ((*ph).hexchat_get_info)(ph, id_cstring.as_ptr()); + let res = ((*ph.ph).hexchat_get_info)(ph.ph, id_cstring.as_ptr()); if res.is_null() { None } else { @@ -782,22 +874,22 @@ impl PluginHandle { // PRIVATE // // ******* // - fn find_valid_context(&mut self) -> Option<Context> { + fn find_valid_context(&mut self) -> Option<Context<'ph>> { unsafe { let ph = self.ph; // TODO wrap this in a safer API, with proper Drop #[allow(unused_mut)] - let mut list = ((*ph).hexchat_list_get)(ph, cstr(b"channels\0")); + let mut list = ((*ph.ph).hexchat_list_get)(ph.ph, cstr(b"channels\0")); // hexchat does this thing where it puts a context in a list_str. // this *is* the proper way to do this - if ((*ph).hexchat_list_next)(ph, list) != 0 { + 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).hexchat_list_str)(ph, list, cstr(b"context\0")) as *const internals::HexchatContext; - ((*ph).hexchat_list_free)(ph, list); + 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)) } else { - ((*ph).hexchat_list_free)(ph, list); + ((*ph.ph).hexchat_list_free)(ph.ph, list); None } } @@ -822,7 +914,7 @@ impl<'a> From<&'a RawAttrs> for EventAttrs<'a> { } } -impl<'a> EnsureValidContext<'a> { +impl<'a, 'ph: 'a> ValidContext<'a, 'ph> { /* * These cause UB: * `hexchat_command` may invalidate the plugin context. @@ -842,12 +934,12 @@ impl<'a> EnsureValidContext<'a> { * 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 EnsureValidContext - things that don't invalidate the + * 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(&mut self, servname: Option<&str>, channel: Option<&str>) -> Option<Context> { + 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; let servname = servname.map(|x| CString::new(x).unwrap()); @@ -855,7 +947,7 @@ impl<'a> EnsureValidContext<'a> { 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).hexchat_find_context)(ph, sptr, cptr) + ((*ph.ph).hexchat_find_context)(ph.ph, sptr, cptr) }; if ctx.is_null() { None @@ -873,7 +965,7 @@ impl<'a> EnsureValidContext<'a> { let nick1 = CString::new(nick1).unwrap(); let nick2 = CString::new(nick2).unwrap(); let res = unsafe { - ((*ph).hexchat_nickcmp)(ph, nick1.as_ptr(), nick2.as_ptr()) + ((*ph.ph).hexchat_nickcmp)(ph.ph, nick1.as_ptr(), nick2.as_ptr()) }; if res < 0 { Ordering::Less @@ -894,7 +986,7 @@ impl<'a> EnsureValidContext<'a> { 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).hexchat_send_modes)(ph, arr.as_mut_ptr(), arr.len() as c_int, + ((*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) } } @@ -906,7 +998,7 @@ impl<'a> EnsureValidContext<'a> { // need to put this in a more permanent position than temporaries let cmd = CString::new(cmd).unwrap(); unsafe { - ((*ph).hexchat_command)(ph, cmd.as_ptr()) + ((*ph.ph).hexchat_command)(ph.ph, cmd.as_ptr()) } } @@ -932,7 +1024,7 @@ impl<'a> EnsureValidContext<'a> { argv[i] = args_cs[i].as_ref().map_or(ptr::null(), |s| s.as_ptr()); } unsafe { - ((*ph).hexchat_emit_print)(ph, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4]) != 0 + ((*ph.ph).hexchat_emit_print)(ph.ph, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4]) != 0 } } @@ -957,9 +1049,9 @@ impl<'a> EnsureValidContext<'a> { for i in 0..4 { argv[i] = args_cs[i].as_ref().map_or(ptr::null(), |s| s.as_ptr()); } - let helper = HexchatEventAttrsHelper::new_with(ph, attrs); + let helper = unsafe { HexchatEventAttrsHelper::new_with(ph.ph, attrs) }; unsafe { - ((*ph).hexchat_emit_print_attrs)(ph, helper.0, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4]) != 0 + ((*ph.ph).hexchat_emit_print_attrs)(ph.ph, helper.0, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4]) != 0 } } @@ -969,14 +1061,14 @@ impl<'a> EnsureValidContext<'a> { // 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 { + pub fn get_context(&mut self) -> Context<'ph> { self.ph.get_context() } /// Sets the current context. /// /// Returns `true` if the context is valid, `false` otherwise. - pub fn set_context(&mut self, ctx: &Context) -> bool { + pub fn set_context(&mut self, ctx: &Context<'ph>) -> bool { self.ph.set_context(ctx) } @@ -986,28 +1078,40 @@ impl<'a> EnsureValidContext<'a> { self.ph.print(s) } + /// 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!`]. + pub fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) { + self.ph.write_fmt(fmt) + } + /// Sets a command hook - pub fn hook_command<F>(&mut self, cmd: &str, cb: F, pri: i32, help: Option<&str>) -> HookHandle where F: Fn(&mut PluginHandle, Word, WordEol) -> Eat + 'static + RefUnwindSafe { - self.ph.hook_command(cmd, cb, pri, help) + 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 { + self.ph.hook_command(cmd, pri, help, cb) } /// Sets a server hook - pub fn hook_server<F>(&mut self, cmd: &str, cb: F, pri: i32) -> HookHandle where F: Fn(&mut PluginHandle, Word, WordEol) -> Eat + 'static + RefUnwindSafe { - self.ph.hook_server(cmd, cb, pri) + 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 { + self.ph.hook_server(cmd, pri, cb) } /// Sets a server hook with attributes - pub fn hook_server_attrs<F>(&mut self, cmd: &str, cb: F, pri: i32) -> HookHandle where F: Fn(&mut PluginHandle, Word, WordEol, EventAttrs) -> Eat + 'static + RefUnwindSafe { - self.ph.hook_server_attrs(cmd, cb, pri) + 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 { + self.ph.hook_server_attrs(cmd, pri, cb) } /// Sets a print hook - pub fn hook_print<F>(&mut self, name: &str, cb: F, pri: i32) -> HookHandle where F: Fn(&mut PluginHandle, Word) -> Eat + 'static + RefUnwindSafe { - self.ph.hook_print(name, cb, pri) + 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 { + self.ph.hook_print(name, pri, cb) } /// Sets a print hook with attributes - pub fn hook_print_attrs<F>(&mut self, name: &str, cb: F, pri: i32) -> HookHandle where F: Fn(&mut PluginHandle, Word, EventAttrs) -> Eat + 'static + RefUnwindSafe { - self.ph.hook_print_attrs(name, cb, pri) + 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 { + self.ph.hook_print_attrs(name, pri, cb) } /// Sets a timer hook - pub fn hook_timer<F>(&mut self, timeout: i32, cb: F) -> HookHandle where F: Fn(&mut PluginHandle) -> bool + 'static + RefUnwindSafe { + pub fn hook_timer<F>(&mut self, timeout: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>) -> bool + 'ph + RefUnwindSafe { self.ph.hook_timer(timeout, cb) } pub fn get_info<'b>(&'b mut self, id: InfoId) -> Option<&'b str> { @@ -1034,7 +1138,9 @@ impl<'a> EnsureValidContext<'a> { // /// Userdata type used by a timer hook. // type TimerHookUd = Box<dyn Fn() -> bool + ::std::panic::RefUnwindSafe>; /// Userdata type used by a hook -type HookUd = Box<dyn Fn(*const *const c_char, *const *const c_char, *const RawAttrs) -> Eat + RefUnwindSafe>; +type HookUd<'ph> = Box<dyn Fn(*const *const c_char, *const *const c_char, *const RawAttrs) -> Eat + RefUnwindSafe + 'ph>; +/// Contexts +type Contexts = Rc<AssertUnwindSafe<RefCell<HashSet<Rc<*const internals::HexchatContext>>>>>; /// The contents of an empty CStr. /// @@ -1067,23 +1173,24 @@ 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: &mut PluginHandle, ctx: *const internals::HexchatContext) -> Context { - let ctxp = AssertUnwindSafe(Rc::new(ctx)); - let weak_ctxp = Rc::downgrade(&ctxp); // calling the Closure should drop the Context (sort of) - let closure: Rc<Cell<Option<HookHandle>>> = Rc::new(Cell::new(None)); - let hook = AssertUnwindSafe(Rc::downgrade(&closure)); // dropping the Context should drop the Closure - ph.skip_pri_ck = true; - closure.set(Some(ph.hook_print("Close Context", move |ph, _| { - // need to be careful not to recurse or leak memory - let ph = ph.ph; - let ctx = ((*ph).hexchat_get_context)(ph); - if **ctxp == ctx { - let _: Option<HookHandle> = hook.upgrade().unwrap().replace(None); - } - EAT_NONE - }, c_int::min_value()))); - ph.skip_pri_ck = false; - Context { ctx: weak_ctxp, closure } +unsafe fn wrap_context<'ph>(ph: &mut 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. @@ -1109,17 +1216,27 @@ unsafe fn hexchat_print_str(ph: *mut RawPh, s: &str, panic_on_nul: bool) { struct HexchatEventAttrsHelper(*mut RawAttrs, *mut RawPh); impl HexchatEventAttrsHelper { - fn new(ph: *mut RawPh) -> Self { - HexchatEventAttrsHelper(unsafe { ((*ph).hexchat_event_attrs_create)(ph) }, ph) + /// 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) } - fn new_with(ph: *mut RawPh, attrs: EventAttrs) -> Self { + /// Creates a new `HexchatEventAttrsHelper` for a given `EventAttrs`. + /// + /// # Safety + /// + /// `ph` must be a valid raw plugin handle. + unsafe fn new_with(ph: *mut RawPh, 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(), Err(_) => 0 }).filter(|&st| st < (time_t::max_value() as u64)).unwrap() as time_t; - unsafe { (*helper.0).server_time_utc = v; } + (*helper.0).server_time_utc = v; helper } } @@ -1133,8 +1250,11 @@ impl Drop for HexchatEventAttrsHelper { } /// Plugin data stored in the hexchat plugin_handle. -struct PhUserdata { - plug: Box<dyn Plugin>, +struct PhUserdata<'ph> { + plug: 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>, pluginfo: PluginInfo, } @@ -1145,8 +1265,8 @@ struct PhUserdata { /// 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: *mut RawPh, ud: Rc<PhUserdata>) { - (*ph).userdata = Rc::into_raw(ud) as *mut c_void; +unsafe fn put_userdata<'ph>(ph: LtPhPtr<'ph>, ud: Rc<PhUserdata<'ph>>) { + (*ph.ph).userdata = Rc::into_raw(ud) as *mut c_void; } // /// Clones the userdata from the plugin handle. @@ -1168,8 +1288,8 @@ unsafe fn put_userdata(ph: *mut RawPh, ud: Rc<PhUserdata>) { /// # Safety /// /// This function is unsafe because it doesn't check if the pointer is valid. -unsafe fn pop_userdata(ph: *mut RawPh) -> Rc<PhUserdata> { - Rc::from_raw(mem::replace(&mut (*ph).userdata, ptr::null_mut()) as *mut PhUserdata) +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>) } // *********************** // @@ -1177,19 +1297,19 @@ unsafe fn pop_userdata(ph: *mut RawPh) -> Rc<PhUserdata> { // *********************** // #[doc(hidden)] -pub unsafe fn hexchat_plugin_init<T>(plugin_handle: *mut c_void, +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 + Default + 'static { - if plugin_handle.is_null() || plugin_name.is_null() || plugin_desc.is_null() || plugin_version.is_null() { + 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. eprintln!("hexchat_plugin_init called with a null pointer that shouldn't be null - broken hexchat"); // TODO maybe call abort. return 0; } - let ph = plugin_handle as *mut RawPh; + 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! @@ -1206,8 +1326,6 @@ pub unsafe fn hexchat_plugin_init<T>(plugin_handle: *mut c_void, // no filename specified for some reason, but we can still load String::new() // empty string }; - // TODO use filename - let _ = filename; // these may be null, unless initialization is successful. // we set them to null as markers. *plugin_name = ptr::null(); @@ -1239,37 +1357,40 @@ pub unsafe fn hexchat_plugin_init<T>(plugin_handle: *mut c_void, }; let r: thread::Result<Option<Rc<_>>> = { catch_unwind(move || { - let mut pluginhandle = PluginHandle::new(ph, pluginfo); + // 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 = unsafe { ((*ph.ph.ph).hexchat_get_context)(ph.ph.ph) }; + contexts.borrow_mut().remove(&ctx); + EAT_NONE + }); + let contexts = Rc::clone(&pluginhandle.contexts); let plug = T::default(); - if plug.init(&mut pluginhandle, if !arg.is_null() { Some(CStr::from_ptr(arg).to_str().expect("arg not valid utf-8 - broken hexchat")) } else { None }) { + 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 }) { if !(pluginfo.name.is_null() || pluginfo.desc.is_null() || pluginfo.vers.is_null()) { - Some(Rc::new(PhUserdata { plug: Box::new(plug), pluginfo })) + Some(Rc::new(PhUserdata { plug: Box::new(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 @ _)) => { - if (*plugin_name).is_null() || (*plugin_desc).is_null() || (*plugin_version).is_null() { - // TODO deallocate any which are non-null - pluginfo.drop_info(); - 0 - } else { - put_userdata(ph, plug); - 1 - } + put_userdata(plugin_handle, plug); + 1 }, r @ _ => { - // if the initialization fails, deinit doesn't get called, so we need to clean up - // ourselves. - - // TODO might leak pluginfo on panic? - if let Err(_) = r { // TODO try to log panic? } @@ -1279,21 +1400,30 @@ pub unsafe fn hexchat_plugin_init<T>(plugin_handle: *mut c_void, } #[doc(hidden)] -pub unsafe fn hexchat_plugin_deinit<T>(plugin_handle: *mut c_void) -> c_int where T: Plugin { +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.is_null() { - let ph = plugin_handle as *mut RawPh; - // userdata should also never be null. + if !plugin_handle.ph.is_null() { + let ph = plugin_handle.ph as *mut RawPh; + // userdata should also never be null - unless we already unloaded. if !(*ph).userdata.is_null() { { let mut info: Option<PluginInfo> = None; { let mut ausinfo = AssertUnwindSafe(&mut info); safe_to_unload = if catch_unwind(move || { - let userdata = pop_userdata(ph); - **ausinfo = Some(userdata.pluginfo); - userdata.plug.deinit(&mut PluginHandle::new(ph, userdata.pluginfo)); + let userdata = pop_userdata(plugin_handle); + let pluginfo = userdata.pluginfo; + userdata.plug.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 + // deinit panics, altho it's worth noting that a panic + // in deinit followed by a panic in drop will cause an + // abort. + // we return 0 mostly as a hint that something went + // wrong, than anything else. + // we do deliberately leak pluginfo on panic, also. }).is_ok() { 1 } else { 0 }; } if let Some(mut info) = info { @@ -1303,7 +1433,7 @@ pub unsafe fn hexchat_plugin_deinit<T>(plugin_handle: *mut c_void) -> c_int wher } } } else { - eprintln!("null userdata in hexchat_plugin_deinit - broken hexchat or broken hexchat-plugin.rs"); + // this is completely normal if we've panicked before. } } else { eprintln!("hexchat_plugin_deinit called with a null plugin_handle - broken hexchat"); @@ -1314,18 +1444,18 @@ pub unsafe fn hexchat_plugin_deinit<T>(plugin_handle: *mut c_void) -> c_int wher /// Exports a hexchat plugin. #[macro_export] macro_rules! hexchat_plugin { - ($t:ty) => { + ($l:lifetime, $t:ty) => { #[no_mangle] - pub unsafe extern "C" fn hexchat_plugin_init(plugin_handle: *mut $crate::c_void, + 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::<$t>(plugin_handle, plugin_name, plugin_desc, plugin_version, arg) + $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(plugin_handle: *mut $crate::c_void) -> $crate::c_int { - $crate::hexchat_plugin_deinit::<$t>(plugin_handle) + 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. diff --git a/src/word.rs b/src/word.rs index 8bcabfc..9e484d7 100644 --- a/src/word.rs +++ b/src/word.rs @@ -1,5 +1,5 @@ // This file is part of Hexchat Plugin API Bindings for Rust -// Copyright (C) 2018, 2021 Soni L. +// 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 Affero General Public License as @@ -22,7 +22,7 @@ use std::ops::Deref; /// # Examples /// /// ```no_run -/// use hexchat_plugin::{PluginHandle, Word, WordEol, Eat}; +/// use hexchat_unsafe_plugin::{PluginHandle, Word, WordEol, Eat}; /// /// fn cmd_foo(ph: &mut PluginHandle, arg: Word, arg_eol: WordEol) -> Eat { /// if arg.len() < 3 { @@ -30,14 +30,14 @@ use std::ops::Deref; /// } else { /// ph.print(&format!("{} {} {}", arg[0], arg[1], arg[2])); /// } -/// hexchat_plugin::EAT_ALL +/// hexchat_unsafe_plugin::EAT_ALL /// } /// /// fn on_privmsg(ph: &mut PluginHandle, word: Word, word_eol: WordEol) -> Eat { /// if word.len() > 0 && word[0].starts_with('@') { /// ph.print("We have message tags!?"); /// } -/// hexchat_plugin::EAT_NONE +/// hexchat_unsafe_plugin::EAT_NONE /// } /// ``` pub struct Word<'a> { @@ -49,7 +49,7 @@ pub struct Word<'a> { /// # Examples /// /// ```no_run -/// use hexchat_plugin::{PluginHandle, Word, WordEol, Eat}; +/// use hexchat_unsafe_plugin::{PluginHandle, Word, WordEol, Eat}; /// /// fn cmd_foo(ph: &mut PluginHandle, arg: Word, arg_eol: WordEol) -> Eat { /// if arg.len() < 3 { @@ -57,14 +57,14 @@ pub struct Word<'a> { /// } else { /// ph.print(&format!("{} {} {}", arg[0], arg[1], arg_eol[2])); /// } -/// hexchat_plugin::EAT_ALL +/// hexchat_unsafe_plugin::EAT_ALL /// } /// /// fn on_privmsg(ph: &mut PluginHandle, word: Word, word_eol: WordEol) -> Eat { /// if word_eol.len() > 0 && word[0].starts_with('@') { /// ph.print("We have message tags!?"); /// } -/// hexchat_plugin::EAT_NONE +/// hexchat_unsafe_plugin::EAT_NONE /// } /// ``` pub struct WordEol<'a> { |