summary refs log blame commit diff stats
path: root/src/lib.rs
blob: a4ef9e933cf3e8fd933cab500050d16afaa42899 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
                                       
                                         













                                                                           















                                                                                                   
             
                
                                       
   
                      
                         
                                                                  

                      
                          
                                                
     
   
                                                    
                                                                                                              
                                                                 
                                                                                                                                                                 
                                          

                                              



                
                                        


                                                                           
 



                                                 

                                                                             
  




                                                                              
  

                                                                      

                  





















                                                                              
  


                                                                            
  

                                                                           
  


                                                                            






                                                                               











                                                                                               


                                      
                                                          



                                                                                           



                                    
                         
                                                            
                                                                              
                                                                        



                                                                 
                             






                                                                 

                                
                              


                                                            

                                                             
                                







                                      
                                 
                                                                        

                                                                                            


                                                                                               


           


                      
        
                
           
              
             
          
         


                       
                
                 

                         

                                             
 
                     
                    

                              
                          
                              
             
                             
             
                           
                                                                            
                  
             
                
                            


                                                  
 


                                              











                                                   


            
 

         














                                     












                                                                               

                                                                               
                              
                                        
                                                                                                         



                                                              
                                                                 
                   
     

 

          


                                                                       
                         
              
                    


                                                                   
                                                                 

                                                                               
                                             

 



                                                                              
             
                                              




                                                          
                              

                       
                       



                            























                                                                         
 
                                    
 
                     
                    






                                        
                                            
                                                                                     
                                              
                     
                                      
                          
                                                                             







                                                                                                   


              
                



                                                  

 

                                                                            
                                   
                                     
 

        



           
                                



                                   
 
                                                 




















                                                                      
                                                          
                        


                              
         
                             
                
                                                                                           

                                                            
                                  


         
 







                                                                             









                                                                       


















                                                                           

                               

                    
         
     

 
                             




                                                            
                                                                                                  
                      
                                                 


         
                                                                                                  



                                                                  


                  
                 
                                                
       
                                            


                                                         
                                                                   
                                                                                       
                


                                                                                              
             









                                                                                     





                                                              
                                                                                       


                                                                                              

                                                                                    

                                                                                           








                                                              
                                                                                       


                                                                                              

                                                                                    

                                                                                                  








                                                              
                                                                                       


                                                                                              

                                                                                    

                                                                                              


         




                                                                        
                                                                                                                 
                                     









                                                                                                                                             




























                                                                                                                                       












                                                                                                                                            

                                    
                                                                               
              

                                                                  


                                                                              
                                                                      
                                                                               




                                                                  
                                                               

                                              
                                                              


                 




                                        

                                                                            


                

                                                                            
       

                                                                           

                                            
             
                                                                                                                                                             


                                       
                                                     


         

                            



                                                           

                  
                 
                                                              
       
                                                                                            
                 
                                                                                                                                
                                          

                                              
             

           
                                                                                                                                                                                                      
                                                                                    


                                                                                                                             
         
                             
                               
                                 
                                                     

                                                       
                                                    
                        

                                                                           






                                                              

                                                                   
                                 
                
                                                                                                                                                                               
                                    
                                                                                             

         
                           


                  
                 
                                                              
       
                                                                                                
                 
                                                                                             


                                                               

                                               
             

           
                                                                                                                                                                                 
                                                                                    
                                                                           

                                            
                                                                                                                                                                                                   
                                                                                    


                                                                                                                                                     
         
                             
                               
                                 
                                                     

                                                           
                                                    
                        

                                                                           







                                                              


                                              
                                                                                                                     
                                    
                                                                                             

         
                          


                  
                 
                                                              
       
                                                                                               
                 
                                                                                         

                                                 
                                                             

                     

                                               
             

           
                                                                                                                                                                        
                                                                                    


                                                                                             


                                                                                             
         
                             
                               
                                 
                                                     

                                                
                                                    
                        

                                                                           





                                                   


                                               
                                                                                                              
                                    
                                                                                             
         

                                           



                  
                                                              
       
                                                                                               
                 
                                                                                                      

                                                 
                                                             

                     

                                               


             
                                                                                                                                                                                          
                                                                                    


                                                                                                                     
         
                             
                               
                                 
                                                     

                                                    
                                                    
                        

                                                                           






                                                     


                                               
                                                                                                                    
                                    
                                                                                             


                         


                  
                 
                                                              
       
                                                                                          
                 
                                      

                                      

               

           
                                                                                                                                                           
                                                                                    







                                                                       
                               
                                 
                                                     



                                                                
                                                    
                                  

                                                                           














                                                                         
                                                       


                                                                        
                     
                 


                   
                                 
                              
                
                                                                                                   
                                    
                                                                                

         

                                     


                                                                               
                              
                
                                                       


         







                                                                              



















                                                                        

     














                                                                          

                                                            
                                                                            


                              
                                                     







































































                                                                              
             







                                                                           
         


































                                                                              

     



                 
                                                          
                

                                                  

                                                              
                                                                         
                                                                             


                                                                                                             
                    
                    


                                                    



         
                         
                                    






                                

                                                    






                                                                                                                                             
                                         


















                                                                                                   
                                                                                        


                                                              
                                                                 

                                                                                                       

                                                                  
                          

                                                                           
                                                          



                          
                                                       
         

     
                                                              
                                                                             
                               
                          



                                                                         
                                                                         







                             

     
                                                                                                                
                          



                                                                                           

                                                                                   
                

                                                                                 
         

     
                           









                                                               
                                     
                         

                                                                         
                
                                                       


         











                                                                                       
                                                                                              
                         














                                                                                              
                                                            



                                                                              
                                                                                                              
         

     
















                                                                                                                    
                                                                                                                       
                         














                                                                                                
                                                            


                                                                              
                                                                             
                
                                                                                                                              
         




                  

                                                                                                   
 



                                               





                                                                  






                                                                             
                                                               



                                     


                                            

                        
 
                                     


                                                            

                                        



                                                                              
                                                      


                              


                                           
                                                                                                                                                                                                      
                                                
     


                                          
                                                                                                                                                                                 
                                         
     


                                                
                                                                                                                                                                                                   
                                               
     


                                         
                                                                                                                                                                        
                                         
     


                                               
                                                                                                                                                                                          
                                               
     


                                         
                                                                                                                                                           

                                       



                                                                       

                            











                                                          






                                               
                                            




                                                                                                   







                                                                                                   
                                                                                                                       

                                                                                             












                                                          
                                            


                                                  





                                                                             

                                                  

 




                                                                              
                                                                                                            
















                                                                  

 








                                                                         
                                                                          






                                                                     

 
                                   
                                                                                            
 
                                                              




                                                       

                                                                             

     




                                                                         
                                                                                  



                                                                                                     
                                                                              
                                        



              
                                                                       

                        
                                                              



         
                                                    
                        
                                          

                                                                          
                                        









                                                                             

                                                                         

 




                                                                             

                                                                                                





                             
              
                                                                      



                                                                        

                                                                                                                 
                                            
                                                                                                            


                                 
                                            


                                                                                                 

















                                                                                  
                                        



                                                                                                  


                                          




                                                                                     





                         




                                                                                                          
                                             
                              






                                                                                                          
                                                                       



                                                             

                                                                                                                                                                                        
                                                                                                     
                                                                                                        



                                                        
                    


                                                                                                     

                    



                                               

                                              

                  
                               
                                         
                               
             
             
          



              
                                                                                                        
                               
                                                            


                                                                           



                                                        
                                                                  
                                                                 
                                                                       
                                                         
                                                                                                                                      








                                                                              






                                           




                                                                                               
                 

                
                                                                  

            
                                                                                             
     
                  




                             
                             
                    
                                                                                            



                                                                                            
                                                                                                               

                    

                                                                                                                 




                                                                                                   
// Hexchat Plugin API Bindings for Rust
// 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
// 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/>.

//! Write hexchat plugins in Rust!
//!
//! This library provides safe API bindings for hexchat, but doesn't attempt to fix hexchat's own
//! bugs. It makes no effort to stop you from unloading your own code while it's still running, for
//! example.
//!
//! When using this library, it's strongly recommended to avoid heap-allocated statics (static
//! mutexes, lazy_static, etc). This is because it's currently impossible to deallocate those on
//! plugin unload. This can be worked around by placing those statics as fields in your plugin
//! struct.
//!
//! This caveat does not apply to static assets (`static FOO: &'static str`, for example), but it
//! does apply to thread-local storage.
//! 
//! # Examples
//!
//! ```no_run
//! #[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, 'ph>>>
//! }
//!
//! unsafe impl<'ph> Plugin<'ph> for MyPlugin<'ph> {
//!     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!");
//!             hexchat_unsafe_plugin::EAT_ALL
//!         }));
//!         true
//!     }
//! }
//!
//! hexchat_plugin!('ph, MyPlugin<'ph>);
//!
//! # fn main() { } // satisfy the compiler, we can't actually run the code
//! ```

/*
 * 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.
 *
 * 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.
 *
 * 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_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_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.
 *
 * 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.
 */

/*
 * Some info about how we do things:
 *
 * DO NOT CALL printf/commandf/etc FAMILY OF FUNCTIONS. You can't avoid allocations, so just
 * allocate some CStrings on the Rust side. It has the exact same effect, since those functions
 * allocate internally anyway.
 */

/*
 * Big list o' TODO:
 * -[ ] Finish API support. [PRI-HIGH]
 *     -[x] word
 *     -[x] word_eol
 *     -[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.)
 *     -[x] hexchat_print (for printf, use print(&format!("...")), it is equivalent.)
 *     -[x] hexchat_emit_print
 *     -[x] hexchat_emit_print_attrs
 *     -[x] hexchat_send_modes
 *     -[x] hexchat_nickcmp
 *     -[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.
 *     -[#] 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
 *     -[x] ~~hexchat_list_free~~ not available - use Drop impls.
 *     -[x] hexchat_hook_command
 *     -[ ] hexchat_hook_fd
 *     -[x] hexchat_hook_print
 *     -[x] hexchat_hook_print_attrs
 *     -[#] hexchat_hook_server (implemented through _attrs)
 *     -[x] hexchat_hook_server_attrs
 *     -[x] hexchat_hook_timer
 *     -[x] ~~hexchat_unhook~~ not available - use Drop impls
 *     -[x] hexchat_find_context
 *     -[x] hexchat_get_context
 *     -[x] hexchat_set_context
 *     -[ ] hexchat_pluginpref_set_str
 *     -[ ] hexchat_pluginpref_get_str
 *     -[ ] hexchat_pluginpref_set_int
 *     -[ ] hexchat_pluginpref_get_int
 *     -[ ] hexchat_pluginpref_delete
 *     -[ ] hexchat_pluginpref_list
 *     -[x] hexchat_plugingui_add
 *     -[x] ~~hexchat_plugingui_remove~~ not available - use Drop impls.
 * -[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.
 * -[ ] ???
 */

#[doc(hidden)]
pub extern crate libc;

mod eat;
mod extra_tests;
mod infoid;
mod internals;
mod pluginfo;
mod strip;
mod word;

pub use eat::*;
pub use infoid::InfoId;
pub use word::*;
pub use strip::*;

use pluginfo::PluginInfo;
use internals::Ph as RawPh;
use internals::HexchatEventAttrs as RawAttrs;

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;
use std::str::FromStr;
use std::thread;
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 //
// ****** //

// Consts

// PRI_*
/// Equivalent to HEXCHAT_PRI_HIGHEST
pub const PRI_HIGHEST: i32 = 127;
/// Equivalent to HEXCHAT_PRI_HIGH
pub const PRI_HIGH: i32 = 64;
/// Equivalent to HEXCHAT_PRI_NORM
pub const PRI_NORM: i32 = 0;
/// Equivalent to HEXCHAT_PRI_LOW
pub const PRI_LOW: i32 = -64;
/// Equivalent to HEXCHAT_PRI_LOWEST
pub const PRI_LOWEST: i32 = -128;

// Traits

/// A hexchat 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.
///
/// 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: 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: Pin<&mut 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`.
// 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,
    // 'ph has to be invariant because RawPh is self-referential.
    // "ideally" we'd want `&'ph mut RawPh<'ph>`, tho the `*mut` above would've
    // had the same effect if `RawPh` were `RawPh<'ph>`.
    _lt: PhantomData<fn(&'ph ()) -> &'ph ()>,
}

/// A hexchat plugin handle, used to register hooks and interact with hexchat.
///
/// # Examples
///
/// ```no_run
/// use hexchat_unsafe_plugin::{PluginHandle};
///
/// fn init(ph: &mut PluginHandle) {
///     ph.register("myplug", "0.1.0", "my awesome plug");
/// }
/// ```
pub struct PluginHandle<'ph> {
    data: LtPhPtr<'ph>,
    plugin: *mut RawPh,
    contexts: Contexts,
    // Used for registration
    info: PluginInfo,
}

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.
    pub server_time: Option<SystemTime>,
    _dummy: PhantomData<&'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, 'f> where 'f: '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>>>,
}

/// 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.
#[derive(Clone)]
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>(F);

// Enums

// ***** //
// impls //
// ***** //

impl<F> InvalidContextError<F> {
    pub fn get_closure(self) -> F {
        self.0
    }
}

impl<'ph, 'f> HookHandle<'ph, 'f> where 'f: 'ph {
    /// If this is a timer hook, returns whether the hook has expired.
    ///
    /// Otherwise, returns false.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use hexchat_unsafe_plugin::{HookHandle};
    ///
    /// /// Remove timers that have expired.
    /// fn clean_up_timers(timers: &mut Vec<HookHandle<'_, '_>>) {
    ///     timers.retain(|timer| {
    ///         !timer.expired()
    ///     });
    /// }
    /// ```
    pub fn expired(&self) -> bool {
        self.freed.get()
    }
}

impl<'ph, 'f> Drop for HookHandle<'ph, 'f> where 'f: 'ph {
    fn drop(&mut self) {
        if self.freed.get() {
            // already free'd.
            return;
        }
        self.freed.set(true);
        unsafe {
            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));
        }
    }
}

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
        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
///
/// `ph` must be a valid pointer (see `std::ptr::read`).
unsafe fn call_hook_protected<F: FnOnce() -> Eat + UnwindSafe>(
    ph: *mut RawPh,
    f: F
) -> Eat {
    match catch_unwind(f) {
        Result::Ok(v @ _) => v,
        Result::Err(e @ _) => {
            // if it's a &str or String, just print it
            if let Some(s) = e.downcast_ref::<&str>() {
                hexchat_print_str(ph, s, false);
            } else if let Some(s) = e.downcast_ref::<String>() {
                hexchat_print_str(ph, &s, false);
            } else if let Some(s) = e.downcast_ref::<Cow<'static, str>>() {
                hexchat_print_str(ph, &s, false);
            } else {
                mem::forget(e);
            }
            EAT_NONE
        }
    }
}

impl<'ph> PluginHandle<'ph> {
    /// Wraps the raw handle.
    ///
    /// # Safety
    ///
    /// `ph` must be a valid pointer (see `std::ptr::read`).
    unsafe fn new(data: LtPhPtr<'ph>, info: PluginInfo, contexts: Contexts) -> PluginHandle<'ph> {
        PluginHandle {
            data, plugin: data.ph, info, contexts
        }
    }

    /// Registers this hexchat plugin. This must be called exactly once when the plugin is loaded.
    ///
    /// # Panics
    ///
    /// This function panics if this plugin is already registered.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use hexchat_unsafe_plugin::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() {
                panic!("Attempt to re-register a plugin");
            }
            let name = CString::new(name).unwrap();
            let desc = CString::new(desc).unwrap();
            let ver = CString::new(ver).unwrap();
            // these shouldn't panic. if they do, we'll need to free them afterwards.
            (*info.name) = name.into_raw();
            (*info.desc) = desc.into_raw();
            (*info.vers) = ver.into_raw();
        }
    }

    /// Returns this plugin's registered name.
    ///
    /// # Panics
    ///
    /// 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() {
                std::str::from_utf8_unchecked(CStr::from_ptr(*info.name).to_bytes())
            } else {
                panic!("Attempt to get the name of a plugin that was not yet registered.");
            }
        }
    }

    /// Returns this plugin's registered description.
    ///
    /// # Panics
    ///
    /// 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() {
                std::str::from_utf8_unchecked(CStr::from_ptr(*info.desc).to_bytes())
            } else {
                panic!("Attempt to get the description of a plugin that was not yet registered.");
            }
        }
    }

    /// Returns this plugin's registered version.
    ///
    /// # Panics
    ///
    /// 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() {
                std::str::from_utf8_unchecked(CStr::from_ptr(*info.vers).to_bytes())
            } else {
                panic!("Attempt to get the version of a plugin that was not yet registered.");
            }
        }
    }

    /// Ensures the current context is valid.
    ///
    /// # Panics
    ///
    /// This function may panic if it's called while hexchat is closing.
    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();
        let res = self.with_context(&ctx, f);
        match res {
            Result::Ok(r @ _) => r,
            Result::Err(e @ _) => {
                let nctx = self.find_valid_context().expect("ensure_valid_context failed (find_valid_context failed), was hexchat closing?");
                self.with_context(&nctx, e.get_closure()).ok().expect("ensure_valid_context failed, was hexchat closing?")
            }
        }
    }

    /// Operates on a virtual plugin.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use hexchat_unsafe_plugin::{PluginHandle};
    ///
    /// fn contexts_can_be_passed_around(ph: &mut PluginHandle<'_>) {
    ///     let ctx = ph.get_context();
    ///     let mut vplug = ph.plugingui_add("foo", "bar", "baz", "qux");
    ///     ph.for_entry_mut(&mut vplug, |ph| {
    ///         ph.set_context(&ctx);
    ///         ph.get_context()
    ///     });
    /// }
    /// ```
    pub fn for_entry_mut<F, R>(&mut self, entry: &mut PluginEntryHandle<'ph>, f: F) -> R where F: FnOnce(&mut PluginHandle<'ph>) -> R {
        // we're doing something kinda (very) weird here, but this is the only
        // way to get and set pluginprefs for virtual plugins. (not that we
        // support those yet...)
        // 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 [`Self::set_context`] to
    /// check.
    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 { ph_call!(hexchat_set_context(self, 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<'ph>) -> bool {
        if let Some(ctx) = ctx.ctx.upgrade() {
            unsafe {
                ph_call!(hexchat_set_context(self, *ctx)) != 0
            }
        } else {
            false
        }
    }

    /// Do something in a valid context.
    ///
    /// 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, returns `Ok(f(...))`.
    ///
    /// Note that `InvalidContextError` contains the original closure. This
    /// allows you to retry, if you so wish.
    ///
    /// [`set_context`]: #method.set_context
    #[inline]
    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(unsafe { ValidContext::new(self) }))
        }
    }

    /// 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, 'ph>> {
    ///     vec![
    ///     ph.hook_command("hello-world", hexchat_unsafe_plugin::PRI_NORM, Some("prints 'Hello, World!'"), |ph, arg, arg_eol| {
    ///         ph.print("Hello, World!");
    ///         hexchat_unsafe_plugin::EAT_ALL
    ///     }),
    ///     ]
    /// }
    /// ```
    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, 'f: 'ph {
        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.data;
            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.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)
                    })
                }
            }))
        };
        let name = CString::new(cmd).unwrap();
        let help_text = help.map(CString::new).map(Result::unwrap);
        let bp = Rc::into_raw(b);
        unsafe {
            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.data, hh: res, freed: Default::default(), _f: PhantomData }
        }
    }
    /// Sets a server hook.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle};
    ///
    /// 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('@') {
    ///             ph.print("We have message tags!?");
    ///         }
    ///         hexchat_unsafe_plugin::EAT_NONE
    ///     }),
    ///     ]
    /// }
    /// ```
    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, 'f: 'ph {
        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, F>(&self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol, EventAttrs) -> Eat + 'f + RefUnwindSafe, 'f: 'ph {
        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.data;
            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.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();
                        cb(&mut ph, word, word_eol, attrs)
                    })
                }
            }))
        };
        let name = CString::new(cmd).unwrap();
        let bp = Rc::into_raw(b);
        unsafe {
            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.data, hh: res, freed: Default::default(), _f: PhantomData }
        }
    }
    /// Sets a print hook.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle};
    ///
    /// 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) {
    ///             if *nick == "KnOwN_SpAmMeR" {
    ///                 return hexchat_unsafe_plugin::EAT_ALL
    ///             }
    ///         }
    ///         hexchat_unsafe_plugin::EAT_NONE
    ///     }),
    ///     ]
    /// }
    /// ```
    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, 'f: 'ph {
        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. :/
        unsafe extern "C" fn callback(word: *const *const c_char, ud: *mut c_void) -> c_int {
            let f: Rc<HookUd> = rc_clone_from_raw(ud as *const HookUd);
            (f)(word, ptr::null(), ptr::null()).do_eat as c_int
        }
        let b: Rc<HookUd> = {
            let ph = self.data;
            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.ph, move || {
                        let mut ph = PluginHandle::new(ph, info, contexts);
                        let word = Word::new(word);
                        cb(&mut ph, word)
                    })
                }
            }))
        };
        let name = CString::new(name).unwrap();
        let bp = Rc::into_raw(b);
        unsafe {
            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.data, hh: res, freed: Default::default(), _f: PhantomData }
        }
    }
    /// Sets a print hook, with attributes.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle};
    ///
    /// 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) {
    ///             if *nick == "KnOwN_SpAmMeR" {
    ///                 return hexchat_unsafe_plugin::EAT_ALL
    ///             }
    ///         }
    ///         hexchat_unsafe_plugin::EAT_NONE
    ///     }),
    ///     ]
    /// }
    /// ```
    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, 'f: 'ph {
        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.data;
            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.ph, move || {
                        let mut ph = PluginHandle::new(ph, info, contexts);
                        let word = Word::new(word);
                        let attrs = (&*attrs).into();
                        cb(&mut ph, word, attrs)
                    })
                }
            }))
        };
        let name = CString::new(name).unwrap();
        let bp = Rc::into_raw(b);
        unsafe {
            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.data, hh: res, freed: Default::default(), _f: PhantomData }
        }
    }
    /// Sets a timer hook
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use hexchat_unsafe_plugin::{PluginHandle, HookHandle};
    ///
    /// fn register_timers<'ph>(ph: &mut PluginHandle<'ph>) -> Vec<HookHandle<'ph, 'ph>> {
    ///     vec![
    ///     ph.hook_timer(2000, |ph| {
    ///         ph.print("timer up!");
    ///         false
    ///     }),
    ///     ]
    /// }
    /// ```
    pub fn hook_timer<'f, F>(&self, timeout: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>) -> bool + 'f + RefUnwindSafe, 'f: 'ph {
        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
        }
        let freed = Rc::new(Cell::new(false));
        // helps us clean up the thing when returning false
        let dropper = Rc::new(Cell::new(None));
        let b: Rc<HookUd> = {
            let ph = self.data;
            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.ph, move || {
                        let mut ph = PluginHandle::new(ph, info, contexts);
                        if cb(&mut ph) {
                            EAT_HEXCHAT
                        } else {
                            EAT_NONE
                        }
                    })
                };
                if res == EAT_NONE && !freed.get() {
                    freed.set(true);
                    unsafe {
                        // drop may panic
                        // and so may unwrap
                        // 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.ph, || {
                            drop(Rc::from_raw(dropper.take().unwrap()));
                            EAT_NONE
                        });
                    }
                }
                res
            }))
        };
        let bp = Rc::into_raw(b);
        dropper.set(Some(bp));
        unsafe {
            let res = ph_call!(hexchat_hook_timer(self, timeout as c_int, callback, bp as *mut _));
            assert!(!res.is_null());
            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>(&self, s: T) {
        let s = s.to_string();
        unsafe {
            hexchat_print_str(self.data.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!`].
    ///
    /// # 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 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_call!(hexchat_get_info(self, id_cstring.as_ptr()));
            if res.is_null() {
                None
            } else {
                let s = CStr::from_ptr(res).to_str();
                // 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(&self) -> Option<Context<'ph>> {
        unsafe {
            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_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, 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 {
                None
            };
            ph_call!(hexchat_list_free(self, list));
            ctx.map(|ctx| wrap_context(self, ctx))
        }
    }
}

impl<'a> EventAttrs<'a> {
    pub fn new() -> EventAttrs<'a> {
        EventAttrs {
            server_time: None,
            _dummy: PhantomData,
        }
    }
}

impl<'a> From<&'a RawAttrs> for EventAttrs<'a> {
    fn from(other: &'a RawAttrs) -> EventAttrs<'a> {
        EventAttrs {
            server_time: if other.server_time_utc > 0 { Some(UNIX_EPOCH + Duration::from_secs(other.server_time_utc as u64)) } else { None },
            _dummy: PhantomData,
        }
    }
}

impl<'a, 'ph: 'a> ValidContext<'a, 'ph> {
/*
 * 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.)
 *
 * 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(&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_call!(hexchat_find_context(ph, sptr, cptr))
        };
        if ctx.is_null() {
            None
        } else {
            Some(unsafe { wrap_context(self.ph, ctx) })
        }
    }

    /// Compares two nicks based on the server's case mapping.
    pub fn nickcmp(&self, nick1: &str, nick2: &str) -> ::std::cmp::Ordering {
        use std::cmp::Ordering;
        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_call!(hexchat_nickcmp(ph, nick1.as_ptr(), nick2.as_ptr()))
        };
        if res < 0 {
            Ordering::Less
        } else if res > 0 {
            Ordering::Greater
        } else {
            Ordering::Equal
        }
    }

    pub fn send_modes<'b, I: IntoIterator<Item=&'b str>>(&mut self, iter: I, mpl: i32, sign: char, mode: char) {
        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");
        let v: Vec<CString> = iter.into_iter().map(|s| CString::new(s).unwrap()).collect();
        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_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.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use hexchat_unsafe_plugin::{ValidContext};
    ///
    /// fn join(context: ValidContext<'_, '_>, channel: &str) {
    ///     context.command(&format!("join {}", channel));
    /// }
    /// ```
    pub fn command(self, cmd: &str) {
        let ph = self.ph;
        // need to put this in a more permanent position than temporaries
        let cmd = CString::new(cmd).unwrap();
        unsafe {
            ph_call!(hexchat_command(ph, cmd.as_ptr()))
        }
    }

    /// Prints an event message, and returns a success status (whether or not
    /// the event exists).
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use hexchat_unsafe_plugin::{ValidContext};
    ///
    /// fn emit_channel_message(context: ValidContext<'_, '_>, nick: &str, msg: &str) {
    ///     context.emit_print("Channel Message", [nick, msg]);
    /// }
    /// ```
    pub fn emit_print<'b, I: IntoIterator<Item=&'b str>>(self, event: &str, args: I) -> bool {
        let ph = self.ph;
        let event = CString::new(event).unwrap();
        let mut args_cs: [Option<CString>; 4] = [None, None, None, None];
        {
            let mut iter = args.into_iter();
            for i in 0..4 {
                args_cs[i] = iter.next().map(|x| CString::new(x).unwrap());
                if args_cs[i].is_none() {
                    break;
                }
            }
            if iter.next().is_some() {
                // it's better to panic so as to get bug reports when we need to increase this
                panic!("too many arguments to emit_print (max 4), or iterator not fused");
            }
        }
        let mut argv: [*const c_char; 5] = [ptr::null(); 5];
        for i in 0..4 {
            argv[i] = args_cs[i].as_ref().map_or(ptr::null(), |s| s.as_ptr());
        }
        unsafe {
            ph_call!(hexchat_emit_print(ph, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4])) != 0
        }
    }

    /// Prints an event message, and returns a success status (whether or not
    /// the event exists).
    ///
    /// Also allows setting event attributes.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use std::time::SystemTime;
    /// use hexchat_unsafe_plugin::{EventAttrs, ValidContext};
    ///
    /// fn emit_channel_message_at(context: ValidContext<'_, '_>, time: Option<SystemTime>, nick: &str, msg: &str) {
    ///     let mut attrs = EventAttrs::new();
    ///     attrs.server_time = time;
    ///     context.emit_print_attrs(attrs, "Channel Message", [nick, msg]);
    /// }
    /// ```
    pub fn emit_print_attrs<'b, I: IntoIterator<Item=&'b str>>(self, attrs: EventAttrs, event: &str, args: I) -> bool {
        let ph = self.ph;
        let event = CString::new(event).unwrap();
        let mut args_cs: [Option<CString>; 4] = [None, None, None, None];
        {
            let mut iter = args.into_iter();
            for i in 0..4 {
                args_cs[i] = iter.next().map(|x| CString::new(x).unwrap());
                if args_cs[i].is_none() {
                    break;
                }
            }
            if let Some(_) = iter.next() {
                // it's better to panic so as to get bug reports when we need to increase this
                panic!("too many arguments to emit_print_attrs (max 4), or iterator not fused");
            }
        }
        let mut argv: [*const c_char; 5] = [ptr::null(); 5];
        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, attrs) };
        unsafe {
            ph_call!(hexchat_emit_print_attrs(ph, helper.0, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4])) != 0
        }
    }

    // ******** //
    // FORWARDS //
    // ******** //
    // We can't just deref because then you could recursively ensure valid context and then it'd no
    // longer work.

    /// 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.
    ///
    /// See [`PluginHandle::print`].
    pub fn print<T: ToString>(&self, s: T) {
        self.ph.print(s)
    }

    /// 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(&self, fmt: fmt::Arguments<'_>) {
        self.ph.write_fmt(fmt)
    }

    /// 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, 'f: 'ph {
        self.ph.hook_command(cmd, pri, help, cb)
    }
    /// 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, 'f: 'ph {
        self.ph.hook_server(cmd, pri, cb)
    }
    /// 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, 'f: 'ph {
        self.ph.hook_server_attrs(cmd, pri, cb)
    }
    /// 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, 'f: 'ph {
        self.ph.hook_print(name, pri, cb)
    }
    /// 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, 'f: 'ph {
        self.ph.hook_print_attrs(name, pri, cb)
    }
    /// 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, 'f: 'ph {
        self.ph.hook_timer(timeout, cb)
    }
    /// 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)
    }
}

// ******* //
// PRIVATE //
// ******* //

// Type aliases, used for safety type checking.
// /// Userdata type used by a command hook.
// We actually do want RefUnwindSafe. This function may be called multiple times, and it's not
// automatically invalidated if it panics, so it may be called again after it panics. If you need
// mutable state, std provides std::sync::Mutex which has poisoning. Other interior mutability with
// poisoning could also be used. std doesn't have anything for single-threaded performance (yet),
// but hexchat isn't particularly performance-critical.
// type CommandHookUd = Box<dyn Fn(Word, WordEol) -> Eat + ::std::panic::RefUnwindSafe>;
// /// Userdata type used by a server hook.
// type ServerHookUd = Box<dyn Fn(Word, WordEol, EventAttrs) -> Eat + ::std::panic::RefUnwindSafe>;
// /// Userdata type used by a print hook.
// type PrintHookUd = Box<dyn Fn(Word, EventAttrs) -> Eat + ::std::panic::RefUnwindSafe>;
// /// Userdata type used by a timer hook.
// type TimerHookUd = Box<dyn Fn() -> bool + ::std::panic::RefUnwindSafe>;
/// Userdata type used by a hook
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>>>>>;

/// The contents of an empty CStr.
///
/// This is useful where you need a non-null CStr.
// NOTE: MUST BE b"\0"!
const EMPTY_CSTRING_DATA: &[u8] = b"\0";

/// Converts a nul-terminated const bstring to a C string.
///
/// # Panics
///
/// Panics if b contains embedded nuls.
// TODO const fn, once that's possible
fn cstr(b: &'static [u8]) -> *const c_char {
    CStr::from_bytes_with_nul(b).unwrap().as_ptr()
}

/// Clones an Rc straight from a raw pointer.
///
/// # Safety
///
/// This function is unsafe because `ptr` must hame come from `Rc::into_raw`.
unsafe fn rc_clone_from_raw<T>(ptr: *const T) -> Rc<T> {
    let rc = ManuallyDrop::new(Rc::from_raw(ptr));
    Rc::clone(&rc)
}

/// Converts a **valid** context pointer into a Context (Rust-managed) struct.
///
/// # Safety
///
/// This function doesn't validate the context.
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 }
    } 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.
///
/// # Safety
///
/// This function does not check the passed in argument.
///
/// # Panics
///
/// Panics if panic_on_nul is true and the string contains embedded nuls.
unsafe fn hexchat_print_str(ph: *mut RawPh, s: &str, panic_on_nul: bool) {
    match CString::new(s) {
        Result::Ok(cs @ _) => {
            let csr: &CStr = &cs;
            ((*ph).hexchat_print)(ph, csr.as_ptr())
        },
        e @ _ => if panic_on_nul {e.unwrap();}, // TODO nul_position?
    }
}

/// Helper to manage owned RawAttrs
struct HexchatEventAttrsHelper<'a, 'ph>(*mut RawAttrs, &'a PluginHandle<'ph>) where 'ph: 'a;

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: &'a PluginHandle<'ph>) -> Self {
        HexchatEventAttrsHelper(ph_call!(hexchat_event_attrs_create(ph)), ph)
    }

    /// Creates a new `HexchatEventAttrsHelper` for a given `EventAttrs`.
    ///
    /// # Safety
    ///
    /// `ph` must be a valid raw plugin handle.
    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(),
            Err(_) => 0
        }).filter(|&st| st < (time_t::max_value() as u64)).unwrap() as time_t;
        (*helper.0).server_time_utc = v;
        helper
    }
}

impl<'a, 'ph> Drop for HexchatEventAttrsHelper<'a, 'ph> where 'ph: 'a {
    fn drop(&mut self) {
        unsafe {
            ph_call!(hexchat_event_attrs_free(self.1, self.0))
        }
    }
}

/// Plugin data stored in the hexchat plugin_handle.
struct PhUserdata<'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, 'ph>,
    pluginfo: PluginInfo,
}

/// Puts the userdata in the plugin handle.
///
/// # Safety
///
/// 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: Box<PhUserdata<'ph>>) {
    (*ph.ph).userdata = Box::into_raw(ud) as *mut c_void;
}

/// 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>) -> Box<PhUserdata<'ph>> {
    Box::from_raw(mem::replace(&mut (*ph.ph).userdata, ptr::null_mut()) as *mut PhUserdata<'ph>)
}

// *********************** //
// PUBLIC OUT OF NECESSITY //
// *********************** //

#[doc(hidden)]
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<'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.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!
    (*ph).userdata = ptr::null_mut();
    // read the filename so we can pass it on later.
    let filename = if !(*plugin_name).is_null() {
        if let Ok(fname) = CStr::from_ptr(*plugin_name).to_owned().into_string() {
            fname
        } else {
            eprintln!("failed to convert filename to utf8 - broken hexchat");
            return 0;
        }
    } else {
        // no filename specified for some reason, but we can still load
        String::new() // empty string
    };
    // these may be null, unless initialization is successful.
    // we set them to null as markers.
    *plugin_name = ptr::null();
    *plugin_desc = ptr::null();
    *plugin_version = ptr::null();
    // do some version checks for safety
    // NOTE: calling hexchat functions with null plugin_name, plugin_desc, plugin_version is a bit
    // dangerous. this particular case is "ok".
    {
        let ver = ((*ph).hexchat_get_info)(ph, cstr(b"version\0")); // this shouldn't panic
        let cstr = CStr::from_ptr(ver);
        if let Ok(ver) = cstr.to_str() {
            let mut iter = ver.split('.');
            let a = iter.next().map(i32::from_str).and_then(Result::ok).unwrap_or(0);
            let b = iter.next().map(i32::from_str).and_then(Result::ok).unwrap_or(0);
            let c = iter.next().map(i32::from_str).and_then(Result::ok).unwrap_or(0);
            // 2.9.6 or greater
            if !(a > 2 || (a == 2 && (b > 9 || (b == 9 && (c > 6 || (c == 6)))))) {
                return 0;
            }
        } else {
            return 0;
        }
    }
    let mut pluginfo = if let Some(pluginfo) = PluginInfo::new(plugin_name, plugin_desc, plugin_version) {
        pluginfo
    } else {
        return 0;
    };
    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()));
            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_call!(hexchat_get_context(ph)) };
                contexts.borrow_mut().remove(&ctx);
                EAT_NONE
            });
            let contexts = Rc::clone(&pluginhandle.contexts);
            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(Box::new(PhUserdata { 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 @ _)) => {
            put_userdata(plugin_handle, plug);
            1
        },
        r @ _ => {
            if let Err(e) = r {
                // TODO try to log panic?
                mem::forget(e);
            }
            0
        },
    }
}

#[doc(hidden)]
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.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 = match catch_unwind(move || {
                        let mut userdata = pop_userdata(plugin_handle);
                        let pluginfo = userdata.pluginfo;
                        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
                        // 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.
                    }) {
                        Ok(_) => 1,
                        Err(e) => {
                            mem::forget(e);
                            0
                        }
                    };
                }
                if let Some(mut info) = info {
                    info.drop_info();
                } else {
                    eprintln!("I have no idea tbh, I didn't know `pop_userdata` could panic!");
                }
            }
        } else {
            // this is completely normal if we've panicked before.
        }
    } else {
        eprintln!("hexchat_plugin_deinit called with a null plugin_handle - broken hexchat");
    }
    safe_to_unload
}

/// Exports a hexchat plugin.
#[macro_export]
macro_rules! hexchat_plugin {
    ($l:lifetime, $t:ty) => {
        #[no_mangle]
        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::<$l, $t>(plugin_handle, plugin_name, plugin_desc, plugin_version, arg)
        }
        #[no_mangle]
        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.
    };
}