summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorSoniEx2 <endermoneymod@gmail.com>2022-04-11 19:33:04 -0300
committerSoniEx2 <endermoneymod@gmail.com>2022-04-11 22:39:16 -0300
commit207026ae5e35da096b8110afe1ba2973a0e54fdb (patch)
tree2612bfe820324034716409a27c5317efa39ba9a4 /src
parent3b38f6e4f418baafe48989a90ce17661a6c2966f (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.rs592
-rw-r--r--src/word.rs14
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> {