summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorSoniEx2 <endermoneymod@gmail.com>2022-04-15 23:49:31 -0300
committerSoniEx2 <endermoneymod@gmail.com>2022-04-15 23:49:31 -0300
commit2b65b6bda3a3467a088aba7ed31e5990971bfe31 (patch)
tree8f93e0abca0b9c726c5eecc1e6926cedfd2cd88a
parent52a1dc21f2fa6131458dcf9b303c595cb9ce6d42 (diff)
Clean up, support virtual plugins, Pin, etc
-rw-r--r--Cargo.toml5
-rw-r--r--src/lib.rs553
-rw-r--r--src/strip.rs134
3 files changed, 541 insertions, 151 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 193c430..4c06152 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "hexchat-unsafe-plugin"
-version = "1.0.0"
+version = "2.0.0"
 authors = ["SoniEx2 <endermoneymod@gmail.com>"]
 description = "Lets you write native HexChat plugins in mostly-safe Rust"
 license = "GPL-3.0-or-later"
@@ -10,5 +10,8 @@ keywords = ["hexchat", "plugin", "hexchat-plugin"]
 categories = ["api-bindings"]
 readme = "README.md"
 
+[features]
+nul_in_strip = []
+
 [dependencies]
 libc = "0.2"
diff --git a/src/lib.rs b/src/lib.rs
index 4dc1bfe..f539a58 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -34,16 +34,17 @@
 //! #[macro_use]
 //! extern crate hexchat_unsafe_plugin;
 //!
+//! use std::pin::Pin;
 //! use std::sync::Mutex;
 //! use hexchat_unsafe_plugin::{Plugin, PluginHandle, HookHandle};
 //!
 //! #[derive(Default)]
 //! struct MyPlugin<'ph> {
-//!     cmd: Mutex<Option<HookHandle<'ph>>>
+//!     cmd: Mutex<Option<HookHandle<'ph, 'ph>>>
 //! }
 //!
 //! unsafe impl<'ph> Plugin<'ph> for MyPlugin<'ph> {
-//!     fn init(&self, ph: &mut PluginHandle<'ph>, filename: &str, arg: Option<&str>) -> bool {
+//!     fn init(self: Pin<&mut Self>, ph: &mut PluginHandle<'ph>, filename: &str, arg: Option<&str>) -> bool {
 //!         ph.register("myplugin", "0.1.0", "my simple plugin");
 //!         *self.cmd.lock().unwrap() = Some(ph.hook_command("hello-world", hexchat_unsafe_plugin::PRI_NORM, Some("prints 'Hello, World!'"), |ph, arg, arg_eol| {
 //!             ph.print("Hello, World!");
@@ -108,6 +109,13 @@
  * We can add the "close context" hook as the first thing when registering a
  * plugin, and invalidate known contexts from it. this is because hexchat
  * always adds new hooks with the same priority before old hooks.
+ *
+ * Borrowing can be scary. Things that can invalidate borrows must be &mut, but
+ * borrows by themselves are generally fine. Unless they're not.
+ *
+ * Some get_info calls may be unsound on some platforms when threads are
+ * involved, as they use getenv. This should be fixed by libc. We still need to
+ * copy them into a String/CString.
  */
 
 /*
@@ -132,14 +140,22 @@
  *     -[x] hexchat_emit_print_attrs
  *     -[x] hexchat_send_modes
  *     -[x] hexchat_nickcmp
- *     -[ ] hexchat_strip
+ *     -[x] hexchat_strip
  *     -[x] ~~hexchat_free~~ not available - use Drop impls.
  *     -[x] ~~hexchat_event_attrs_create~~ not available - converted as needed
  *     -[x] ~~hexchat_event_attrs_free~~ not available - use Drop impls.
- *     -[x] hexchat_get_info
+ *     -[#] hexchat_get_info (with the below as separate methods)
+ *         -[x] libdirfs
+ *         -[ ] gtkwin_ptr
+ *         -[ ] win_ptr
  *     -[ ] hexchat_get_prefs
- *     -[ ] hexchat_list_get, hexchat_list_fields, hexchat_list_next, hexchat_list_str,
- *         hexchat_list_int, hexchat_list_time, hexchat_list_free
+ *     -[ ] hexchat_list_get
+ *     -[ ] hexchat_list_fields
+ *     -[ ] hexchat_list_next
+ *     -[ ] hexchat_list_str
+ *     -[ ] hexchat_list_int
+ *     -[ ] hexchat_list_time
+ *     -[x] ~~hexchat_list_free~~ not available - use Drop impls.
  *     -[x] hexchat_hook_command
  *     -[ ] hexchat_hook_fd
  *     -[x] hexchat_hook_print
@@ -157,10 +173,10 @@
  *     -[ ] hexchat_pluginpref_get_int
  *     -[ ] hexchat_pluginpref_delete
  *     -[ ] hexchat_pluginpref_list
- *     -[ ] hexchat_plugingui_add
+ *     -[x] hexchat_plugingui_add
  *     -[x] ~~hexchat_plugingui_remove~~ not available - use Drop impls.
- * -[ ] Wrap contexts within something we keep track of. Mark them invalid when contexts are
- *     closed. [PRI-MAYBE]
+ * -[x] Wrap contexts within something we keep track of. Mark them invalid when contexts are
+ *     closed.
  * -[x] Anchor closures on the stack using Rc<T>. Many (most?) hooks are reentrant. As far as I
  *     know, all of them need this.
  *     -[x] Additionally, use a Cell<bool> for timers.
@@ -175,10 +191,12 @@ mod infoid;
 mod internals;
 mod pluginfo;
 mod word;
+mod strip;
 
 pub use eat::*;
 pub use infoid::InfoId;
 pub use word::*;
+pub use strip::*;
 
 use pluginfo::PluginInfo;
 use internals::Ph as RawPh;
@@ -188,12 +206,14 @@ use std::borrow::Cow;
 use std::cell::Cell;
 use std::cell::RefCell;
 use std::collections::HashSet;
+use std::convert::TryInto;
 use std::ffi::{CString, CStr};
 use std::fmt;
 use std::marker::PhantomData;
 use std::mem;
 use std::mem::ManuallyDrop;
 use std::panic::{AssertUnwindSafe, RefUnwindSafe, UnwindSafe, catch_unwind};
+use std::pin::Pin;
 use std::ptr;
 use std::rc::Rc;
 use std::rc::Weak as RcWeak;
@@ -204,6 +224,18 @@ use std::time::{SystemTime, UNIX_EPOCH, Duration};
 #[doc(hidden)]
 pub use libc::{c_char, c_int, c_void, time_t};
 
+// private macros
+
+/// Calls a function on a PluginHandle struct.
+macro_rules! ph_call {
+    ($f:ident($ph:expr, $($args:expr),*)) => {
+        ((*$ph.data.ph).$f)($ph.plugin, $($args),*)
+    };
+    ($f:ident($ph:expr $(,)?)) => {
+        ((*$ph.data.ph).$f)($ph.plugin)
+    };
+}
+
 // ****** //
 // PUBLIC //
 // ****** //
@@ -238,14 +270,16 @@ pub const PRI_LOWEST: i32 = -128;
 /// inherently unsafe!
 ///
 /// At least Unsafe Rust is still safer than writing C. So you have that going.
+///
+/// TL;DR: Either don't use threads, or ensure they're dead in `Drop`/`deinit`.
 pub unsafe trait Plugin<'ph> {
     /// Called to initialize the plugin.
-    fn init(&self, ph: &mut PluginHandle<'ph>, filename: &str, arg: Option<&str>) -> bool;
+    fn init(self: Pin<&mut Self>, ph: &mut PluginHandle<'ph>, filename: &str, arg: Option<&str>) -> bool;
 
     /// Called to deinitialize the plugin.
     ///
     /// This is always called immediately prior to Drop::drop.
-    fn deinit(&self, ph: &mut PluginHandle<'ph>) {
+    fn deinit(self: Pin<&mut Self>, ph: &mut PluginHandle<'ph>) {
         let _ = ph;
     }
 }
@@ -255,12 +289,12 @@ pub unsafe trait Plugin<'ph> {
 /// A `*mut RawPh` with a lifetime bolted to it.
 ///
 /// This allows us to enforce a non-`'static` lifetime on the `Plugin`.
-#[repr(transparent)]
+// this is NOT public API
 #[doc(hidden)]
+#[repr(transparent)]
 #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
 pub struct LtPhPtr<'ph> {
     ph: *mut RawPh,
-    // FIXME we may want a different signature here (&'a Cell<RawPh>?)
     _lt: PhantomData<&'ph RawPh>,
 }
 
@@ -276,7 +310,8 @@ pub struct LtPhPtr<'ph> {
 /// }
 /// ```
 pub struct PluginHandle<'ph> {
-    ph: LtPhPtr<'ph>,
+    data: LtPhPtr<'ph>,
+    plugin: *mut RawPh,
     contexts: Contexts,
     // Used for registration
     info: PluginInfo,
@@ -320,12 +355,19 @@ pub struct EventAttrs<'a> {
 
 /// A hook handle, used to enable unhooking.
 #[must_use = "Hooks must be stored somewhere and are automatically unhooked on Drop"]
-pub struct HookHandle<'ph> {
+pub struct HookHandle<'ph, 'f> where 'ph: 'f {
     ph: LtPhPtr<'ph>,
     hh: *const internals::HexchatHook,
     freed: Rc<Cell<bool>>,
     // this does actually store an Rc<...>, but on the other side of the FFI.
-    _f: PhantomData<Rc<HookUd<'ph>>>,
+    _f: PhantomData<Rc<HookUd<'f>>>,
+}
+
+/// A virtual plugin.
+#[must_use = "Virtual plugins must be stored somewhere and are automatically unregistered on Drop"]
+pub struct PluginEntryHandle<'ph> {
+    ph: LtPhPtr<'ph>,
+    entry: *const internals::PluginGuiHandle,
 }
 
 /// A context.
@@ -353,7 +395,7 @@ impl<F> InvalidContextError<F> {
     }
 }
 
-impl<'ph> Drop for HookHandle<'ph> {
+impl<'ph, 'f> Drop for HookHandle<'ph, 'f> where 'ph: 'f {
     fn drop(&mut self) {
         if self.freed.get() {
             // already free'd.
@@ -361,7 +403,7 @@ impl<'ph> Drop for HookHandle<'ph> {
         }
         self.freed.set(true);
         unsafe {
-            let b = ((*self.ph.ph).hexchat_unhook)(self.ph.ph, self.hh) as *mut HookUd<'ph>;
+            let b = ((*self.ph.ph).hexchat_unhook)(self.ph.ph, self.hh) as *mut HookUd<'f>;
             // we assume b is not null. this should be safe.
             // just drop it
             drop(Rc::from_raw(b));
@@ -369,6 +411,14 @@ impl<'ph> Drop for HookHandle<'ph> {
     }
 }
 
+impl<'ph> Drop for PluginEntryHandle<'ph> {
+    fn drop(&mut self) {
+        unsafe {
+            ((*self.ph.ph).hexchat_plugingui_remove)(self.ph.ph, self.entry);
+        }
+    }
+}
+
 impl<'ph> Drop for Context<'ph> {
     fn drop(&mut self) {
         // check if we need to clean anything up
@@ -410,9 +460,9 @@ impl<'ph> PluginHandle<'ph> {
     /// # Safety
     ///
     /// `ph` must be a valid pointer (see `std::ptr::read`).
-    unsafe fn new(ph: LtPhPtr<'ph>, info: PluginInfo, contexts: Contexts) -> PluginHandle<'ph> {
+    unsafe fn new(data: LtPhPtr<'ph>, info: PluginInfo, contexts: Contexts) -> PluginHandle<'ph> {
         PluginHandle {
-            ph, info, contexts
+            data, plugin: data.ph, info, contexts
         }
     }
 
@@ -427,11 +477,12 @@ impl<'ph> PluginHandle<'ph> {
     /// ```no_run
     /// use hexchat_unsafe_plugin::PluginHandle;
     ///
-    /// fn init(ph: &mut PluginHandle) {
+    /// fn init(ph: &mut PluginHandle<'_>) {
     ///     ph.register("foo", "0.1.0", "my foo plugin");
     /// }
     /// ```
     pub fn register(&mut self, name: &str, desc: &str, ver: &str) {
+        assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't be registered");
         unsafe {
             let info = self.info;
             if !(*info.name).is_null() || !(*info.desc).is_null() || !(*info.vers).is_null() {
@@ -453,6 +504,7 @@ impl<'ph> PluginHandle<'ph> {
     ///
     /// This function panics if this plugin is not registered.
     pub fn get_name(&self) -> &str {
+        assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't be registered");
         unsafe {
             let info = self.info;
             if !(*info.name).is_null() || !(*info.desc).is_null() || !(*info.vers).is_null() {
@@ -469,6 +521,7 @@ impl<'ph> PluginHandle<'ph> {
     ///
     /// This function panics if this plugin is not registered.
     pub fn get_description(&self) -> &str {
+        assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't be registered");
         unsafe {
             let info = self.info;
             if !(*info.name).is_null() || !(*info.desc).is_null() || !(*info.vers).is_null() {
@@ -485,6 +538,7 @@ impl<'ph> PluginHandle<'ph> {
     ///
     /// This function panics if this plugin is not registered.
     pub fn get_version(&self) -> &str {
+        assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't be registered");
         unsafe {
             let info = self.info;
             if !(*info.name).is_null() || !(*info.desc).is_null() || !(*info.vers).is_null() {
@@ -512,18 +566,42 @@ impl<'ph> PluginHandle<'ph> {
         }
     }
 
+    /// Operates on a virtual plugin.
+    pub fn for_entry_mut<F, R>(&mut self, entry: &mut PluginEntryHandle<'ph>, f: F) -> R where F: for<'eh> FnOnce(&mut PluginHandle<'eh>) -> R {
+        // we're doing something kinda (very) weird here, but this is the only
+        // way to get and set pluginprefs for virtual plugins.
+        // this should be sound.
+        let data = self.data;
+        let info = self.info;
+        let contexts = Rc::clone(&self.contexts);
+        let mut handle = unsafe { Self::new(data, info, contexts) };
+        handle.plugin = entry.entry as *mut RawPh;
+        handle.set_context(&self.get_context());
+        f(&mut handle)
+    }
+
+    /// Registers a virtual plugin.
+    pub fn plugingui_add(&self, filename: &str, name: &str, description: &str, version: &str) -> PluginEntryHandle<'ph> {
+        let filename = CString::new(filename).unwrap();
+        let name = CString::new(name).unwrap();
+        let description = CString::new(description).unwrap();
+        let version = CString::new(version).unwrap();
+        let res = unsafe {
+            ph_call!(hexchat_plugingui_add(self, filename.as_ptr(), name.as_ptr(), description.as_ptr(), version.as_ptr(), ptr::null_mut()))
+        };
+        PluginEntryHandle { ph: self.data, entry: res }
+    }
+
     /// Returns the current context.
     ///
-    /// Note: The returned context may be invalid. Use [`set_context`] to
+    /// Note: The returned context may be invalid. Use [`Self::set_context`] to
     /// check.
-    ///
-    /// [`set_context`]: #method.set_context
-    pub fn get_context(&mut self) -> Context<'ph> {
-        let ctxp = unsafe { ((*self.ph.ph).hexchat_get_context)(self.ph.ph) };
+    pub fn get_context(&self) -> Context<'ph> {
+        let ctxp = unsafe { ph_call!(hexchat_get_context(self)) };
         // This needs to be fixed by hexchat. I cannot make the value become
         // null when it's invalid without invoking UB. This is because I can't
         // set_context to null.
-        let ok = unsafe { ((*self.ph.ph).hexchat_set_context)(self.ph.ph, ctxp) };
+        let ok = unsafe { ph_call!(hexchat_set_context(self, ctxp)) };
         unsafe { wrap_context(self, if ok == 0 { ptr::null() } else { ctxp }) }
     }
 
@@ -533,7 +611,7 @@ impl<'ph> PluginHandle<'ph> {
     pub fn set_context(&mut self, ctx: &Context<'ph>) -> bool {
         if let Some(ctx) = ctx.ctx.upgrade() {
             unsafe {
-                ((*self.ph.ph).hexchat_set_context)(self.ph.ph, *ctx) != 0
+                ph_call!(hexchat_set_context(self, *ctx)) != 0
             }
         } else {
             false
@@ -565,12 +643,16 @@ impl<'ph> PluginHandle<'ph> {
 
     /// Sets a command hook.
     ///
+    /// # Panics
+    ///
+    /// Panics if this is a borrowed [`PluginEntryHandle`].
+    ///
     /// # Examples
     ///
     /// ```no_run
     /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle};
     ///
-    /// fn register_commands<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph>> {
+    /// fn register_commands<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph, 'ph>> {
     ///     vec![
     ///     ph.hook_command("hello-world", hexchat_unsafe_plugin::PRI_NORM, Some("prints 'Hello, World!'"), |ph, arg, arg_eol| {
     ///         ph.print("Hello, World!");
@@ -579,13 +661,14 @@ impl<'ph> PluginHandle<'ph> {
     ///     ]
     /// }
     /// ```
-    pub fn hook_command<F>(&mut self, cmd: &str, pri: i32, help: Option<&str>, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'ph + RefUnwindSafe {
+    pub fn hook_command<'f, F>(&self, cmd: &str, pri: i32, help: Option<&str>, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'f + RefUnwindSafe, 'ph: 'f {
+        assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks");
         unsafe extern "C" fn callback(word: *const *const c_char, word_eol: *const *const c_char, ud: *mut c_void) -> c_int {
             let f: Rc<HookUd> = rc_clone_from_raw(ud as *const HookUd);
             (f)(word, word_eol, ptr::null()).do_eat as c_int
         }
         let b: Rc<HookUd> = {
-            let ph = self.ph;
+            let ph = self.data;
             let info = self.info;
             let contexts = Rc::clone(&self.contexts);
             Rc::new(Box::new(move |word, word_eol, _| {
@@ -605,9 +688,9 @@ impl<'ph> PluginHandle<'ph> {
         let help_text = help.map(CString::new).map(Result::unwrap);
         let bp = Rc::into_raw(b);
         unsafe {
-            let res = ((*self.ph.ph).hexchat_hook_command)(self.ph.ph, name.as_ptr(), pri as c_int, callback, help_text.as_ref().map(|s| s.as_ptr()).unwrap_or(ptr::null()), bp as *mut _);
+            let res = ph_call!(hexchat_hook_command(self, name.as_ptr(), pri as c_int, callback, help_text.as_ref().map(|s| s.as_ptr()).unwrap_or(ptr::null()), bp as *mut _));
             assert!(!res.is_null());
-            HookHandle { ph: self.ph, hh: res, freed: Default::default(), _f: PhantomData }
+            HookHandle { ph: self.data, hh: res, freed: Default::default(), _f: PhantomData }
         }
     }
     /// Sets a server hook.
@@ -617,7 +700,7 @@ impl<'ph> PluginHandle<'ph> {
     /// ```no_run
     /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle};
     ///
-    /// fn register_server_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph>> {
+    /// fn register_server_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph, 'ph>> {
     ///     vec![
     ///     ph.hook_server("PRIVMSG", hexchat_unsafe_plugin::PRI_NORM, |ph, word, word_eol| {
     ///         if word.len() > 0 && word[0].starts_with('@') {
@@ -628,17 +711,19 @@ impl<'ph> PluginHandle<'ph> {
     ///     ]
     /// }
     /// ```
-    pub fn hook_server<F>(&mut self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'ph + RefUnwindSafe {
+    pub fn hook_server<'f, F>(&self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'f + RefUnwindSafe, 'ph: 'f {
+        assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks");
         self.hook_server_attrs(cmd, pri, move |ph, w, we, _| cb(ph, w, we))
     }
     /// Sets a server hook, with attributes.
-    pub fn hook_server_attrs<F>(&mut self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol, EventAttrs) -> Eat + 'ph + RefUnwindSafe {
+    pub fn hook_server_attrs<'f, F>(&self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol, EventAttrs) -> Eat + 'f + RefUnwindSafe, 'ph: 'f {
+        assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks");
         unsafe extern "C" fn callback(word: *const *const c_char, word_eol: *const *const c_char, attrs: *const RawAttrs, ud: *mut c_void) -> c_int {
             let f: Rc<HookUd> = rc_clone_from_raw(ud as *const HookUd);
             (f)(word, word_eol, attrs).do_eat as c_int
         }
         let b: Rc<HookUd> = {
-            let ph = self.ph;
+            let ph = self.data;
             let info = self.info;
             let contexts = Rc::clone(&self.contexts);
             Rc::new(Box::new(move |word, word_eol, attrs| {
@@ -658,9 +743,9 @@ impl<'ph> PluginHandle<'ph> {
         let name = CString::new(cmd).unwrap();
         let bp = Rc::into_raw(b);
         unsafe {
-            let res = ((*self.ph.ph).hexchat_hook_server_attrs)(self.ph.ph, name.as_ptr(), pri as c_int, callback, bp as *mut _);
+            let res = ph_call!(hexchat_hook_server_attrs(self, name.as_ptr(), pri as c_int, callback, bp as *mut _));
             assert!(!res.is_null());
-            HookHandle { ph: self.ph, hh: res, freed: Default::default(), _f: PhantomData }
+            HookHandle { ph: self.data, hh: res, freed: Default::default(), _f: PhantomData }
         }
     }
     /// Sets a print hook.
@@ -670,7 +755,7 @@ impl<'ph> PluginHandle<'ph> {
     /// ```no_run
     /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle};
     ///
-    /// fn register_print_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph>> {
+    /// fn register_print_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph, 'ph>> {
     ///     vec![
     ///     ph.hook_print("Channel Message", hexchat_unsafe_plugin::PRI_NORM, |ph, arg| {
     ///         if let Some(nick) = arg.get(0) {
@@ -683,7 +768,8 @@ impl<'ph> PluginHandle<'ph> {
     ///     ]
     /// }
     /// ```
-    pub fn hook_print<F>(&mut self, name: &str, pri: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word) -> Eat + 'ph + RefUnwindSafe {
+    pub fn hook_print<'f, F>(&self, name: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word) -> Eat + 'f + RefUnwindSafe, 'ph: 'f {
+        assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks");
         // hmm, is there any way to avoid this code duplication?
         // hook_print is special because dummy prints (keypresses, Close Context) are handled
         // through here, but never through hook_print_attrs. :/
@@ -692,7 +778,7 @@ impl<'ph> PluginHandle<'ph> {
             (f)(word, ptr::null(), ptr::null()).do_eat as c_int
         }
         let b: Rc<HookUd> = {
-            let ph = self.ph;
+            let ph = self.data;
             let info = self.info;
             let contexts = Rc::clone(&self.contexts);
             Rc::new(Box::new(move |word, _, _| {
@@ -710,9 +796,9 @@ impl<'ph> PluginHandle<'ph> {
         let name = CString::new(name).unwrap();
         let bp = Rc::into_raw(b);
         unsafe {
-            let res = ((*self.ph.ph).hexchat_hook_print)(self.ph.ph, name.as_ptr(), pri as c_int, callback, bp as *mut _);
+            let res = ph_call!(hexchat_hook_print(self, name.as_ptr(), pri as c_int, callback, bp as *mut _));
             assert!(!res.is_null());
-            HookHandle { ph: self.ph, hh: res, freed: Default::default(), _f: PhantomData }
+            HookHandle { ph: self.data, hh: res, freed: Default::default(), _f: PhantomData }
         }
     }
     /// Sets a print hook, with attributes.
@@ -722,7 +808,7 @@ impl<'ph> PluginHandle<'ph> {
     /// ```no_run
     /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle};
     ///
-    /// fn register_print_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph>> {
+    /// fn register_print_hooks<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph, 'ph>> {
     ///     vec![
     ///     ph.hook_print_attrs("Channel Message", hexchat_unsafe_plugin::PRI_NORM, |ph, arg, attrs| {
     ///         if let Some(nick) = arg.get(0) {
@@ -735,13 +821,14 @@ impl<'ph> PluginHandle<'ph> {
     ///     ]
     /// }
     /// ```
-    pub fn hook_print_attrs<F>(&mut self, name: &str, pri: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word, EventAttrs) -> Eat + 'ph + RefUnwindSafe {
+    pub fn hook_print_attrs<'f, F>(&self, name: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, EventAttrs) -> Eat + 'f + RefUnwindSafe, 'ph: 'f {
+        assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks");
         unsafe extern "C" fn callback(word: *const *const c_char, attrs: *const RawAttrs, ud: *mut c_void) -> c_int {
             let f: Rc<HookUd> = rc_clone_from_raw(ud as *const HookUd);
             (f)(word, ptr::null(), attrs).do_eat as c_int
         }
         let b: Rc<HookUd> = {
-            let ph = self.ph;
+            let ph = self.data;
             let info = self.info;
             let contexts = Rc::clone(&self.contexts);
             Rc::new(Box::new(move |word, _, attrs| {
@@ -760,9 +847,9 @@ impl<'ph> PluginHandle<'ph> {
         let name = CString::new(name).unwrap();
         let bp = Rc::into_raw(b);
         unsafe {
-            let res = ((*self.ph.ph).hexchat_hook_print_attrs)(self.ph.ph, name.as_ptr(), pri as c_int, callback, bp as *mut _);
+            let res = ph_call!(hexchat_hook_print_attrs(self, name.as_ptr(), pri as c_int, callback, bp as *mut _));
             assert!(!res.is_null());
-            HookHandle { ph: self.ph, hh: res, freed: Default::default(), _f: PhantomData }
+            HookHandle { ph: self.data, hh: res, freed: Default::default(), _f: PhantomData }
         }
     }
     /// Sets a timer hook
@@ -772,7 +859,7 @@ impl<'ph> PluginHandle<'ph> {
     /// ```no_run
     /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle};
     ///
-    /// fn register_timers<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph>> {
+    /// fn register_timers<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph, 'ph>> {
     ///     vec![
     ///     ph.hook_timer(2000, |ph| {
     ///         ph.print("timer up!");
@@ -781,7 +868,8 @@ impl<'ph> PluginHandle<'ph> {
     ///     ]
     /// }
     /// ```
-    pub fn hook_timer<F>(&mut self, timeout: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>) -> bool + 'ph + RefUnwindSafe {
+    pub fn hook_timer<'f, F>(&self, timeout: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>) -> bool + 'f + RefUnwindSafe, 'ph: 'f {
+        assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks");
         unsafe extern "C" fn callback(ud: *mut c_void) -> c_int {
             let f: Rc<HookUd> = rc_clone_from_raw(ud as *const HookUd);
             (f)(ptr::null(), ptr::null(), ptr::null()).do_eat as c_int
@@ -790,7 +878,7 @@ impl<'ph> PluginHandle<'ph> {
         // helps us clean up the thing when returning false
         let dropper = Rc::new(Cell::new(None));
         let b: Rc<HookUd> = {
-            let ph = self.ph;
+            let ph = self.data;
             let info = self.info;
             let contexts = Rc::clone(&self.contexts);
             let freed = AssertUnwindSafe(Rc::clone(&freed));
@@ -828,18 +916,19 @@ impl<'ph> PluginHandle<'ph> {
         let bp = Rc::into_raw(b);
         dropper.set(Some(bp));
         unsafe {
-            let res = ((*self.ph.ph).hexchat_hook_timer)(self.ph.ph, timeout as c_int, callback, bp as *mut _);
+            let res = ph_call!(hexchat_hook_timer(self, timeout as c_int, callback, bp as *mut _));
             assert!(!res.is_null());
-            HookHandle { ph: self.ph, hh: res, freed: freed, _f: PhantomData }
+            HookHandle { ph: self.data, hh: res, freed: freed, _f: PhantomData }
         }
     }
 
     /// Prints to the hexchat buffer.
-    // this checks the context internally. if it didn't, it wouldn't be safe to have here.
-    pub fn print<T: ToString>(&mut self, s: T) {
+    // this checks the context internally. if it didn't, it wouldn't be safe to
+    // have here.
+    pub fn print<T: ToString>(&self, s: T) {
         let s = s.to_string();
         unsafe {
-            hexchat_print_str(self.ph.ph, &*s, true);
+            hexchat_print_str(self.data.ph, &*s, true);
         }
     }
 
@@ -851,47 +940,189 @@ impl<'ph> PluginHandle<'ph> {
     ///
     /// This panics if any broken formatting trait implementations are used in
     /// the format arguments. See also [`format!`].
-    pub fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) {
-        self.print(fmt);
+    ///
+    /// # Examples
+    ///
+    /// ```no_run
+    /// use hexchat_unsafe_plugin::PluginHandle;
+    ///
+    /// fn hello_world(ph: &mut PluginHandle<'_>) {
+    ///     write!(ph, "Hello, world!");
+    /// }
+    /// ```
+    pub fn write_fmt(&self, fmt: fmt::Arguments<'_>) {
+        if let Some(s) = fmt.as_str() {
+            // "fast" path. hexchat_print_str still has to allocate, and
+            // hexchat is slow af.
+            unsafe {
+                hexchat_print_str(self.data.ph, s, true);
+            }
+        } else {
+            self.print(fmt);
+        }
     }
 
-    /// Returns information on the current context.
-    pub fn get_info<'a>(&'a mut self, id: InfoId) -> Option<&'a str> {
-        let ph = self.ph;
+    /// Returns context and client information.
+    ///
+    /// See [`InfoId`] for the kinds of information that can be retrieved.
+    ///
+    /// # Examples
+    ///
+    /// ```no_run
+    /// use hexchat_unsafe_plugin::{InfoId, PluginHandle};
+    ///
+    /// /// Returns whether we are currently away.
+    /// fn is_away(ph: &mut PluginHandle<'_>) -> bool {
+    ///     ph.get_info(InfoId::Away).is_some()
+    /// }
+    /// ```
+    pub fn get_info<'a>(&'a self, id: InfoId) -> Option<Cow<'a, str>> {
         let id_cstring = CString::new(&*id.name()).unwrap();
         unsafe {
-            let res = ((*ph.ph).hexchat_get_info)(ph.ph, id_cstring.as_ptr());
+            let res = ph_call!(hexchat_get_info(self, id_cstring.as_ptr()));
             if res.is_null() {
                 None
             } else {
                 let s = CStr::from_ptr(res).to_str();
-                Some(s.expect("non-utf8 word - broken hexchat"))
+                // FIXME: figure out which InfoId's are safe to borrow.
+                Some(s.expect("broken hexchat").to_owned().into())
+            }
+        }
+    }
+
+    /// Returns the path to the plugin directory, used for auto-loading
+    /// plugins.
+    ///
+    /// The returned `CStr` is not guaranteed to be valid UTF-8, but local
+    /// file system encoding.
+    ///
+    /// # Examples
+    ///
+    /// ```no_run
+    /// use std::path::PathBuf;
+    ///
+    /// use hexchat_unsafe_plugin::PluginHandle;
+    ///
+    /// // On Unix, we can treat this as an array-of-bytes filesystem path.
+    /// #[cfg(unix)]
+    /// fn plugin_dir(ph: &PluginHandle<'_>) -> PathBuf {
+    ///     use std::ffi::OsString;
+    ///     use std::os::unix::ffi::OsStringExt;
+    ///
+    ///     let libdirfs = ph.get_libdirfs().expect("should exist");
+    ///     OsString::from_vec(libdirfs.into_bytes()).into()
+    /// }
+    /// ```
+    pub fn get_libdirfs(&self) -> Option<CString> {
+        let id_cstring = CString::new("libdirfs").unwrap();
+        unsafe {
+            let res = ph_call!(hexchat_get_info(self, id_cstring.as_ptr()));
+            if res.is_null() {
+                None
+            } else {
+                Some(CStr::from_ptr(res).to_owned())
+            }
+        }
+    }
+
+    /// Strips attributes from text. See [`Strip`] for which attributes can
+    /// be stripped.
+    ///
+    /// # Examples
+    ///
+    /// ```no_run
+    /// use hexchat_unsafe_plugin::{PluginHandle, Strip};
+    ///
+    /// /// Removes colors
+    /// fn strip_colors(ph: &PluginHandle<'_>, s: &str) -> String {
+    ///     ph.strip(s, Strip::new().colors(true))
+    /// }
+    /// ```
+    #[cfg(feature = "nul_in_strip")]
+    pub fn strip(&self, s: &str, strip: Strip) -> String {
+        // ironically the single API where we can pass embedded NULs.
+        // we also don't need to worry about terminating it with NUL.
+        let mut out = Vec::with_capacity(s.len());
+        let in_ptr = s.as_ptr() as *const _;
+        let in_len = s.len().try_into().unwrap();
+        let flags = strip.flags();
+        // NOTE: avoid panicking from here.
+        let stripped = unsafe {
+            ph_call!(hexchat_strip(self, in_ptr, in_len, flags))
+        };
+        // tho annoyingly we don't know the out len, so we need to count NULs.
+        let in_nuls = s.as_bytes().into_iter().filter(|&&x| x == 0).count();
+        let mut out_len = 0;
+        for _ in 0..=in_nuls {
+            while unsafe { *stripped.add(out_len) } != 0 {
+                out_len += 1;
             }
+            out_len += 1;
+        }
+        out.extend_from_slice(unsafe {
+            // take out the extra NUL at the end.
+            ::std::slice::from_raw_parts(stripped as *const _, out_len - 1)
+        });
+        unsafe {
+            ph_call!(hexchat_free(self, stripped as *const _));
         }
+        // we can panic again here, tho this should never panic.
+        String::from_utf8(out).unwrap()
+    }
+
+    /// Strips attributes from text. See [`Strip`] for which attributes can
+    /// be stripped.
+    ///
+    /// # Examples
+    ///
+    /// ```no_run
+    /// use hexchat_unsafe_plugin::{PluginHandle, Strip};
+    ///
+    /// /// Removes colors
+    /// fn strip_colors(ph: &PluginHandle<'_>, s: &str) -> String {
+    ///     ph.strip(s, Strip::new().colors(true))
+    /// }
+    /// ```
+    #[cfg(not(feature = "nul_in_strip"))]
+    pub fn strip(&self, s: &str, strip: Strip) -> String {
+        // make sure s doesn't contain nuls
+        assert!(!s.as_bytes().contains(&0), "embedded nuls are not allowed");
+        let mut out = Vec::with_capacity(s.len());
+        let in_ptr = s.as_ptr() as *const _;
+        let in_len = s.len().try_into().unwrap();
+        let flags = strip.flags();
+        // NOTE: avoid panicking from here.
+        let stripped = unsafe {
+            ph_call!(hexchat_strip(self, in_ptr, in_len, flags))
+        };
+        out.extend_from_slice(unsafe { CStr::from_ptr(stripped) }.to_bytes());
+        unsafe {
+            ph_call!(hexchat_free(self, stripped as *const _));
+        }
+        // we can panic again here, tho this should never panic.
+        String::from_utf8(out).unwrap()
     }
 
     // ******* //
     // PRIVATE //
     // ******* //
 
-    fn find_valid_context(&mut self) -> Option<Context<'ph>> {
+    fn find_valid_context(&self) -> Option<Context<'ph>> {
         unsafe {
-            let ph = self.ph;
+            let channel_key = cstr(b"channels\0");
+            let context_key = cstr(b"context\0");
             // TODO wrap this in a safer API, with proper Drop
             #[allow(unused_mut)]
-            let mut list = ((*ph.ph).hexchat_list_get)(ph.ph, cstr(b"channels\0"));
+            let mut list = ph_call!(hexchat_list_get(self, channel_key));
             // hexchat does this thing where it puts a context in a list_str.
-            // this *is* the proper way to do this
-            if ((*ph.ph).hexchat_list_next)(ph.ph, list) != 0 {
-                // if this panics we may leak some memory. it's not a big deal tho, as it indicates
-                // a bug in hexchat-plugin.rs.
-                let ctx = ((*ph.ph).hexchat_list_str)(ph.ph, list, cstr(b"context\0")) as *const internals::HexchatContext;
-                ((*ph.ph).hexchat_list_free)(ph.ph, list);
-                Some(wrap_context(self, ctx))
+            // this *is* the proper way to do this, but it looks weird.
+            let ctx = if ph_call!(hexchat_list_next(self, list)) != 0 {
+                Some(ph_call!(hexchat_list_str(self, list, context_key)) as *const internals::HexchatContext)
             } else {
-                ((*ph.ph).hexchat_list_free)(ph.ph, list);
                 None
-            }
+            };
+            ph_call!(hexchat_list_free(self, list));
+            ctx.map(|ctx| wrap_context(self, ctx))
         }
     }
 }
@@ -939,15 +1170,14 @@ impl<'a, 'ph: 'a> ValidContext<'a, 'ph> {
  */
 
     /// Finds an open context for the given servname and channel.
-    pub fn find_context(&mut self, servname: Option<&str>, channel: Option<&str>) -> Option<Context<'ph>> {
-        // this was a mistake but oh well
-        let ph = self.ph.ph;
+    pub fn find_context(&self, servname: Option<&str>, channel: Option<&str>) -> Option<Context<'ph>> {
+        let ph = &self.ph;
         let servname = servname.map(|x| CString::new(x).unwrap());
         let channel = channel.map(|x| CString::new(x).unwrap());
         let ctx = unsafe {
             let sptr = servname.map(|x| x.as_ptr()).unwrap_or(ptr::null());
             let cptr = channel.map(|x| x.as_ptr()).unwrap_or(ptr::null());
-            ((*ph.ph).hexchat_find_context)(ph.ph, sptr, cptr)
+            ph_call!(hexchat_find_context(ph, sptr, cptr))
         };
         if ctx.is_null() {
             None
@@ -957,15 +1187,14 @@ impl<'a, 'ph: 'a> ValidContext<'a, 'ph> {
     }
 
     /// Compares two nicks based on the server's case mapping.
-    pub fn nickcmp(&mut self, nick1: &str, nick2: &str) -> ::std::cmp::Ordering {
+    pub fn nickcmp(&self, nick1: &str, nick2: &str) -> ::std::cmp::Ordering {
         use std::cmp::Ordering;
-        // this was a mistake but oh well
-        let ph = self.ph.ph;
+        let ph = &self.ph;
         // need to put this in a more permanent position than temporaries
         let nick1 = CString::new(nick1).unwrap();
         let nick2 = CString::new(nick2).unwrap();
         let res = unsafe {
-            ((*ph.ph).hexchat_nickcmp)(ph.ph, nick1.as_ptr(), nick2.as_ptr())
+            ph_call!(hexchat_nickcmp(ph, nick1.as_ptr(), nick2.as_ptr()))
         };
         if res < 0 {
             Ordering::Less
@@ -977,8 +1206,7 @@ impl<'a, 'ph: 'a> ValidContext<'a, 'ph> {
     }
 
     pub fn send_modes<'b, I: IntoIterator<Item=&'b str>>(&mut self, iter: I, mpl: i32, sign: char, mode: char) {
-        // this was a mistake but oh well
-        let ph = self.ph.ph;
+        let ph = &self.ph;
         assert!(sign == '+' || sign == '-', "sign must be + or -");
         assert!(mode.is_ascii(), "mode must be ascii");
         assert!(mpl >= 0, "mpl must be non-negative");
@@ -986,24 +1214,23 @@ impl<'a, 'ph: 'a> ValidContext<'a, 'ph> {
         let mut v2: Vec<*const c_char> = (&v).iter().map(|x| x.as_ptr()).collect();
         let arr: &mut [*const c_char] = &mut *v2;
         unsafe {
-            ((*ph.ph).hexchat_send_modes)(ph.ph, arr.as_mut_ptr(), arr.len() as c_int,
-                mpl as c_int, sign as c_char, mode as c_char)
+            ph_call!(hexchat_send_modes(ph, arr.as_mut_ptr(), arr.len() as c_int,
+                mpl as c_int, sign as c_char, mode as c_char))
         }
     }
 
     /// Executes a command.
     pub fn command(self, cmd: &str) {
-        // this was a mistake but oh well
-        let ph = self.ph.ph;
+        let ph = self.ph;
         // need to put this in a more permanent position than temporaries
         let cmd = CString::new(cmd).unwrap();
         unsafe {
-            ((*ph.ph).hexchat_command)(ph.ph, cmd.as_ptr())
+            ph_call!(hexchat_command(ph, cmd.as_ptr()))
         }
     }
 
     pub fn emit_print<'b, I: IntoIterator<Item=&'b str>>(self, event: &str, args: I) -> bool {
-        let ph = self.ph.ph;
+        let ph = self.ph;
         let event = CString::new(event).unwrap();
         let mut args_cs: [Option<CString>; 4] = [None, None, None, None];
         {
@@ -1024,12 +1251,12 @@ impl<'a, 'ph: 'a> ValidContext<'a, 'ph> {
             argv[i] = args_cs[i].as_ref().map_or(ptr::null(), |s| s.as_ptr());
         }
         unsafe {
-            ((*ph.ph).hexchat_emit_print)(ph.ph, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4]) != 0
+            ph_call!(hexchat_emit_print(ph, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4])) != 0
         }
     }
 
     pub fn emit_print_attrs<'b, I: IntoIterator<Item=&'b str>>(self, attrs: EventAttrs, event: &str, args: I) -> bool {
-        let ph = self.ph.ph;
+        let ph = self.ph;
         let event = CString::new(event).unwrap();
         let mut args_cs: [Option<CString>; 4] = [None, None, None, None];
         {
@@ -1049,9 +1276,9 @@ impl<'a, 'ph: 'a> ValidContext<'a, 'ph> {
         for i in 0..4 {
             argv[i] = args_cs[i].as_ref().map_or(ptr::null(), |s| s.as_ptr());
         }
-        let helper = unsafe { HexchatEventAttrsHelper::new_with(ph.ph, attrs) };
+        let helper = unsafe { HexchatEventAttrsHelper::new_with(ph, attrs) };
         unsafe {
-            ((*ph.ph).hexchat_emit_print_attrs)(ph.ph, helper.0, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4]) != 0
+            ph_call!(hexchat_emit_print_attrs(ph, helper.0, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4])) != 0
         }
     }
 
@@ -1061,62 +1288,102 @@ impl<'a, 'ph: 'a> ValidContext<'a, 'ph> {
     // We can't just deref because then you could recursively ensure valid context and then it'd no
     // longer work.
 
-    pub fn get_context(&mut self) -> Context<'ph> {
+    /// Returns the current context.
+    ///
+    /// See [`PluginHandle::get_context`].
+    pub fn get_context(&self) -> Context<'ph> {
         self.ph.get_context()
     }
 
     /// Sets the current context.
     ///
     /// Returns `true` if the context is valid, `false` otherwise.
+    ///
+    /// See [`PluginHandle::set_context`].
+    // One would think this is unsound, but if the given context isn't valid,
+    // the current context is unchanged and still valid.
+    // But if the given context is valid, it becomes the new, valid, current
+    // context.
+    // So either way we're still in a valid context when this returns.
     pub fn set_context(&mut self, ctx: &Context<'ph>) -> bool {
         self.ph.set_context(ctx)
     }
 
     /// Prints to the hexchat buffer.
-    // as of hexchat 2.14.1, this does not call hooks.
-    pub fn print<T: ToString>(&mut self, s: T) {
+    ///
+    /// See [`PluginHandle::print`].
+    pub fn print<T: ToString>(&self, s: T) {
         self.ph.print(s)
     }
 
-    /// Prints to this hexchat buffer.
+    /// Prints to the hexchat buffer.
     ///
     /// Glue for usage of the [`write!`] macro with hexchat.
     ///
+    /// See [`PluginHandle::write_fmt`].
+    ///
     /// # Panics
     ///
     /// This panics if any broken formatting trait implementations are used in
     /// the format arguments. See also [`format!`].
-    pub fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) {
+    pub fn write_fmt(&self, fmt: fmt::Arguments<'_>) {
         self.ph.write_fmt(fmt)
     }
 
-    /// Sets a command hook
-    pub fn hook_command<F>(&mut self, cmd: &str, pri: i32, help: Option<&str>, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'ph + RefUnwindSafe {
+    /// Sets a command hook.
+    ///
+    /// See [`PluginHandle::hook_command`].
+    pub fn hook_command<'f, F>(&self, cmd: &str, pri: i32, help: Option<&str>, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'f + RefUnwindSafe, 'ph: 'f {
         self.ph.hook_command(cmd, pri, help, cb)
     }
-    /// Sets a server hook
-    pub fn hook_server<F>(&mut self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'ph + RefUnwindSafe {
+    /// Sets a server hook.
+    ///
+    /// See [`PluginHandle::hook_server`].
+    pub fn hook_server<'f, F>(&self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol) -> Eat + 'f + RefUnwindSafe, 'ph: 'f {
         self.ph.hook_server(cmd, pri, cb)
     }
-    /// Sets a server hook with attributes
-    pub fn hook_server_attrs<F>(&mut self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol, EventAttrs) -> Eat + 'ph + RefUnwindSafe {
+    /// Sets a server hook with attributes.
+    ///
+    /// See [`PluginHandle::hook_server_attrs`].
+    pub fn hook_server_attrs<'f, F>(&self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol, EventAttrs) -> Eat + 'f + RefUnwindSafe, 'ph: 'f {
         self.ph.hook_server_attrs(cmd, pri, cb)
     }
-    /// Sets a print hook
-    pub fn hook_print<F>(&mut self, name: &str, pri: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word) -> Eat + 'ph + RefUnwindSafe {
+    /// Sets a print hook.
+    ///
+    /// See [`PluginHandle::hook_print`].
+    pub fn hook_print<'f, F>(&self, name: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word) -> Eat + 'f + RefUnwindSafe, 'ph: 'f {
         self.ph.hook_print(name, pri, cb)
     }
-    /// Sets a print hook with attributes
-    pub fn hook_print_attrs<F>(&mut self, name: &str, pri: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>, Word, EventAttrs) -> Eat + 'ph + RefUnwindSafe {
+    /// Sets a print hook with attributes.
+    ///
+    /// See [`PluginHandle::hook_print_attrs`].
+    pub fn hook_print_attrs<'f, F>(&self, name: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, EventAttrs) -> Eat + 'f + RefUnwindSafe, 'ph: 'f {
         self.ph.hook_print_attrs(name, pri, cb)
     }
-    /// Sets a timer hook
-    pub fn hook_timer<F>(&mut self, timeout: i32, cb: F) -> HookHandle<'ph> where F: Fn(&mut PluginHandle<'ph>) -> bool + 'ph + RefUnwindSafe {
+    /// Sets a timer hook.
+    ///
+    /// See [`PluginHandle::hook_timer`].
+    pub fn hook_timer<'f, F>(&self, timeout: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>) -> bool + 'f + RefUnwindSafe, 'ph: 'f {
         self.ph.hook_timer(timeout, cb)
     }
-    pub fn get_info<'b>(&'b mut self, id: InfoId) -> Option<&'b str> {
+    /// Returns context and client information.
+    ///
+    /// See [`PluginHandle::get_info`].
+    pub fn get_info<'b>(&'b self, id: InfoId) -> Option<Cow<'b, str>> {
         self.ph.get_info(id)
     }
+    /// Returns the plugin directory.
+    ///
+    /// See [`PluginHandle::get_libdirfs`].
+    pub fn get_libdirfs(&self) -> Option<CString> {
+        self.ph.get_libdirfs()
+    }
+    /// Strips attributes from text.
+    ///
+    /// See [`PluginHandle::strip`].
+    pub fn strip(&self, s: &str, strip: Strip) -> String {
+        self.ph.strip(s, strip)
+    }
 }
 
 // ******* //
@@ -1138,7 +1405,7 @@ impl<'a, 'ph: 'a> ValidContext<'a, 'ph> {
 // /// Userdata type used by a timer hook.
 // type TimerHookUd = Box<dyn Fn() -> bool + ::std::panic::RefUnwindSafe>;
 /// Userdata type used by a hook
-type HookUd<'ph> = Box<dyn Fn(*const *const c_char, *const *const c_char, *const RawAttrs) -> Eat + RefUnwindSafe + 'ph>;
+type HookUd<'f> = Box<dyn Fn(*const *const c_char, *const *const c_char, *const RawAttrs) -> Eat + RefUnwindSafe + 'f>;
 /// Contexts
 type Contexts = Rc<AssertUnwindSafe<RefCell<HashSet<Rc<*const internals::HexchatContext>>>>>;
 
@@ -1173,7 +1440,7 @@ unsafe fn rc_clone_from_raw<T>(ptr: *const T) -> Rc<T> {
 /// # Safety
 ///
 /// This function doesn't validate the context.
-unsafe fn wrap_context<'ph>(ph: &mut PluginHandle<'ph>, ctx: *const internals::HexchatContext) -> Context<'ph> {
+unsafe fn wrap_context<'ph>(ph: &PluginHandle<'ph>, ctx: *const internals::HexchatContext) -> Context<'ph> {
     let contexts = ph.contexts.clone();
     if ctx.is_null() {
         Context { contexts, ctx: RcWeak::new(), _ph: PhantomData }
@@ -1213,16 +1480,16 @@ unsafe fn hexchat_print_str(ph: *mut RawPh, s: &str, panic_on_nul: bool) {
 }
 
 /// Helper to manage owned RawAttrs
-struct HexchatEventAttrsHelper(*mut RawAttrs, *mut RawPh);
+struct HexchatEventAttrsHelper<'a, 'ph>(*mut RawAttrs, &'a PluginHandle<'ph>) where 'ph: 'a;
 
-impl HexchatEventAttrsHelper {
+impl<'a, 'ph> HexchatEventAttrsHelper<'a, 'ph> where 'ph: 'a {
     /// Creates a new, empty `HexchatEventAttrsHelper`.
     ///
     /// # Safety
     ///
     /// `ph` must be a valid raw plugin handle.
-    unsafe fn new(ph: *mut RawPh) -> Self {
-        HexchatEventAttrsHelper(((*ph).hexchat_event_attrs_create)(ph), ph)
+    unsafe fn new(ph: &'a PluginHandle<'ph>) -> Self {
+        HexchatEventAttrsHelper(ph_call!(hexchat_event_attrs_create(ph)), ph)
     }
 
     /// Creates a new `HexchatEventAttrsHelper` for a given `EventAttrs`.
@@ -1230,7 +1497,7 @@ impl HexchatEventAttrsHelper {
     /// # Safety
     ///
     /// `ph` must be a valid raw plugin handle.
-    unsafe fn new_with(ph: *mut RawPh, attrs: EventAttrs<'_>) -> Self {
+    unsafe fn new_with(ph: &'a PluginHandle<'ph>, attrs: EventAttrs<'_>) -> Self {
         let helper = Self::new(ph);
         let v = attrs.server_time.or(Some(UNIX_EPOCH)).map(|st| match st.duration_since(UNIX_EPOCH) {
             Ok(n) => n.as_secs(),
@@ -1241,20 +1508,20 @@ impl HexchatEventAttrsHelper {
     }
 }
 
-impl Drop for HexchatEventAttrsHelper {
+impl<'a, 'ph> Drop for HexchatEventAttrsHelper<'a, 'ph> where 'ph: 'a {
     fn drop(&mut self) {
         unsafe {
-            ((*self.1).hexchat_event_attrs_free)(self.1, self.0)
+            ph_call!(hexchat_event_attrs_free(self.1, self.0))
         }
     }
 }
 
 /// Plugin data stored in the hexchat plugin_handle.
 struct PhUserdata<'ph> {
-    plug: Box<dyn Plugin<'ph> + 'ph>,
+    plug: Pin<Box<dyn Plugin<'ph> + 'ph>>,
     contexts: Contexts,
     // this is never read, but we need to not drop it until we can drop it
-    _context_hook: HookHandle<'ph>,
+    _context_hook: HookHandle<'ph, 'ph>,
     pluginfo: PluginInfo,
 }
 
@@ -1265,31 +1532,17 @@ struct PhUserdata<'ph> {
 /// This function is unsafe because it doesn't check if the pointer is valid.
 ///
 /// Improper use of this function can leak memory.
-unsafe fn put_userdata<'ph>(ph: LtPhPtr<'ph>, ud: Rc<PhUserdata<'ph>>) {
-    (*ph.ph).userdata = Rc::into_raw(ud) as *mut c_void;
+unsafe fn put_userdata<'ph>(ph: LtPhPtr<'ph>, ud: Box<PhUserdata<'ph>>) {
+    (*ph.ph).userdata = Box::into_raw(ud) as *mut c_void;
 }
 
-// /// Clones the userdata from the plugin handle.
-// /// 
-// /// # Safety
-// ///
-// /// This function is unsafe because it doesn't check if the pointer is valid.
-// unsafe fn clone_userdata(ph: *mut RawPh) -> Option<Rc<PhUserdata>> {
-//     let ptr = (*ph).userdata as *const PhUserdata;
-//     if ptr.is_null() {
-//         None
-//     } else {
-//         Some(rc_clone_from_raw(ptr))
-//     }
-// }
-
 /// Pops the userdata from the plugin handle.
 ///
 /// # Safety
 ///
 /// This function is unsafe because it doesn't check if the pointer is valid.
-unsafe fn pop_userdata<'ph>(ph: LtPhPtr<'ph>) -> Rc<PhUserdata<'ph>> {
-    Rc::from_raw(mem::replace(&mut (*ph.ph).userdata, ptr::null_mut()) as *mut PhUserdata<'ph>)
+unsafe fn pop_userdata<'ph>(ph: LtPhPtr<'ph>) -> Box<PhUserdata<'ph>> {
+    Box::from_raw(mem::replace(&mut (*ph.ph).userdata, ptr::null_mut()) as *mut PhUserdata<'ph>)
 }
 
 // *********************** //
@@ -1355,7 +1608,7 @@ pub unsafe fn hexchat_plugin_init<'ph, T>(plugin_handle: LtPhPtr<'ph>,
     } else {
         return 0;
     };
-    let r: thread::Result<Option<Rc<_>>> = {
+    let r: thread::Result<Option<Box<_>>> = {
         catch_unwind(move || {
             // AssertUnwindSafe not Default at the time of writing this
             let contexts = Rc::new(AssertUnwindSafe(Default::default()));
@@ -1364,15 +1617,15 @@ pub unsafe fn hexchat_plugin_init<'ph, T>(plugin_handle: LtPhPtr<'ph>,
             // must register this before the plugin registers anything else!
             let context_hook = pluginhandle.hook_print("Close Context", c_int::min_value(), move |ph, _| {
                 // just remove the context! it's that simple!
-                let ctx = unsafe { ((*ph.ph.ph).hexchat_get_context)(ph.ph.ph) };
+                let ctx = unsafe { ph_call!(hexchat_get_context(ph)) };
                 contexts.borrow_mut().remove(&ctx);
                 EAT_NONE
             });
             let contexts = Rc::clone(&pluginhandle.contexts);
-            let plug = T::default();
-            if plug.init(&mut pluginhandle, &filename, if !arg.is_null() { Some(CStr::from_ptr(arg).to_str().expect("arg not valid utf-8 - broken hexchat")) } else { None }) {
+            let mut plug = Box::pin(T::default());
+            if plug.as_mut().init(&mut pluginhandle, &filename, if !arg.is_null() { Some(CStr::from_ptr(arg).to_str().expect("arg not valid utf-8 - broken hexchat")) } else { None }) {
                 if !(pluginfo.name.is_null() || pluginfo.desc.is_null() || pluginfo.vers.is_null()) {
-                    Some(Rc::new(PhUserdata { plug: Box::new(plug), pluginfo, contexts, _context_hook: context_hook }))
+                    Some(Box::new(PhUserdata { plug, pluginfo, contexts, _context_hook: context_hook }))
                 } else {
                     // TODO log: forgot to call register
                     None
@@ -1412,9 +1665,9 @@ pub unsafe fn hexchat_plugin_deinit<'ph, T>(plugin_handle: LtPhPtr<'ph>) -> c_in
                 {
                     let mut ausinfo = AssertUnwindSafe(&mut info);
                     safe_to_unload = if catch_unwind(move || {
-                        let userdata = pop_userdata(plugin_handle);
+                        let mut userdata = pop_userdata(plugin_handle);
                         let pluginfo = userdata.pluginfo;
-                        userdata.plug.deinit(&mut PluginHandle::new(plugin_handle, pluginfo, Rc::clone(&userdata.contexts)));
+                        userdata.plug.as_mut().deinit(&mut PluginHandle::new(plugin_handle, pluginfo, Rc::clone(&userdata.contexts)));
                         drop(userdata);
                         **ausinfo = Some(pluginfo);
                         // we drop the plugin regardless of whether or not
diff --git a/src/strip.rs b/src/strip.rs
new file mode 100644
index 0000000..b10fb44
--- /dev/null
+++ b/src/strip.rs
@@ -0,0 +1,134 @@
+// This file is part of Hexchat Plugin API Bindings for Rust
+// Copyright (C) 2022 Soni L.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+/// Stripping mode for [`PluginHandle::strip`](crate::PluginHandle::strip).
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
+pub struct Strip {
+    // 4
+    hidden: bool,
+    // 2
+    formatting: bool,
+    // 1
+    colors: bool,
+}
+
+impl Strip {
+    /// Creates a new `Strip` that, by default, strips no attributes.
+    ///
+    /// # Examples
+    ///
+    /// ```no_run
+    /// use hexchat_unsafe_plugin::{PluginHandle, Strip};
+    ///
+    /// fn strip_nothing(s: &str, ph: &PluginHandle<'_>) -> String {
+    ///     ph.strip(s, Strip::new())
+    /// }
+    /// ```
+    pub const fn new() -> Strip {
+        Strip {
+            hidden: false,
+            formatting: false,
+            colors: false,
+        }
+    }
+
+    /// Sets whether to remove mIRC color attributes.
+    ///
+    /// # Examples
+    ///
+    /// ```no_run
+    /// use hexchat_unsafe_plugin::{PluginHandle, Strip};
+    ///
+    /// fn strip_colors(s: &str, ph: &PluginHandle<'_>) -> String {
+    ///     ph.strip(s, Strip::new().colors(true))
+    /// }
+    /// ```
+    pub const fn colors(mut self, strip: bool) -> Self {
+        self.colors = strip;
+        self
+    }
+
+    /// Sets whether to remove formatting attributes.
+    ///
+    /// # Examples
+    ///
+    /// ```no_run
+    /// use hexchat_unsafe_plugin::{PluginHandle, Strip};
+    ///
+    /// fn strip_formatting(s: &str, ph: &PluginHandle<'_>) -> String {
+    ///     ph.strip(s, Strip::new().formatting(true))
+    /// }
+    /// ```
+    pub const fn formatting(mut self, strip: bool) -> Self {
+        self.formatting = strip;
+        self
+    }
+
+    /// Sets whether to remove internal "hidden text" formatting attributes.
+    ///
+    /// This is split from [`Self::formatting`] because these attributes are
+    /// only processed when writing directly to a buffer - they're for
+    /// internal/plugin use. This tends to be useful when processing user or
+    /// remote input and writing it directly to a buffer.
+    ///
+    /// # Examples
+    ///
+    /// ```no_run
+    /// use hexchat_unsafe_plugin::{PluginHandle, Strip};
+    ///
+    /// fn strip_hidden(s: &str, ph: &PluginHandle<'_>) -> String {
+    ///     ph.strip(s, Strip::new().hidden(true))
+    /// }
+    /// ```
+    pub const fn hidden(mut self, strip: bool) -> Self {
+        self.hidden = strip;
+        self
+    }
+
+    /// Creates a new `Strip` that strips all strippable attributes.
+    ///
+    /// # Examples
+    ///
+    /// ```no_run
+    /// use hexchat_unsafe_plugin::{PluginHandle, Strip};
+    ///
+    /// fn strip_all(s: &str, ph: &PluginHandle<'_>) -> String {
+    ///     ph.strip(s, Strip::all())
+    /// }
+    /// ```
+    pub const fn all() -> Strip {
+        Strip {
+            hidden: true,
+            formatting: true,
+            colors: true,
+        }
+    }
+
+    /// Builds the flags for FFI.
+    pub(crate) fn flags(self) -> ::libc::c_int {
+        let mut value = 0;
+        if self.hidden {
+            value |= 4;
+        }
+        if self.formatting {
+            value |= 2;
+        }
+        if self.colors {
+            value |= 1;
+        }
+        value
+    }
+}