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 /src | |
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.
Diffstat (limited to 'src')
-rw-r--r-- | src/lib.rs | 592 | ||||
-rw-r--r-- | src/word.rs | 14 |
2 files changed, 368 insertions, 238 deletions
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> { |