// 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 .
//! 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>>
//! }
//!
//! 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. Many (most?) hooks are reentrant. As far as I
* know, all of them need this.
* -[x] Additionally, use a Cell for timers.
* -[ ] ???
*/
#[doc(hidden)]
pub extern crate libc;
mod eat;
mod infoid;
mod internals;
mod pluginfo;
mod word;
mod strip;
pub use eat::*;
pub use infoid::InfoId;
pub use word::*;
pub use strip::*;
use pluginfo::PluginInfo;
use internals::Ph as RawPh;
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,
_lt: PhantomData<&'ph RawPh>,
}
/// 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,
_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 'ph: 'f {
ph: LtPhPtr<'ph>,
hh: *const internals::HexchatHook,
freed: Rc>,
// this does actually store an Rc<...>, but on the other side of the FFI.
_f: PhantomData>>,
}
/// 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);
// Enums
// ***** //
// impls //
// ***** //
impl InvalidContextError {
pub fn get_closure(self) -> F {
self.0
}
}
impl<'ph, 'f> Drop for HookHandle<'ph, 'f> where 'ph: 'f {
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 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::() {
hexchat_print_str(ph, &s, false);
} else if let Some(s) = e.downcast_ref::>() {
hexchat_print_str(ph, &s, false);
}
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(&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.
pub fn for_entry_mut(&mut self, entry: &mut PluginEntryHandle<'ph>, f: F) -> R where F: for<'eh> FnOnce(&mut PluginHandle<'eh>) -> R {
// we're doing something kinda (very) weird here, but this is the only
// way to get and set pluginprefs for virtual plugins.
// this should be sound.
let data = self.data;
let info = self.info;
let contexts = Rc::clone(&self.contexts);
let mut handle = unsafe { Self::new(data, info, contexts) };
handle.plugin = entry.entry as *mut RawPh;
handle.set_context(&self.get_context());
f(&mut handle)
}
/// Registers a virtual plugin.
pub fn plugingui_add(&self, filename: &str, name: &str, description: &str, version: &str) -> PluginEntryHandle<'ph> {
let filename = CString::new(filename).unwrap();
let name = CString::new(name).unwrap();
let description = CString::new(description).unwrap();
let version = CString::new(version).unwrap();
let res = unsafe {
ph_call!(hexchat_plugingui_add(self, filename.as_ptr(), name.as_ptr(), description.as_ptr(), version.as_ptr(), ptr::null_mut()))
};
PluginEntryHandle { ph: self.data, entry: res }
}
/// Returns the current context.
///
/// Note: The returned context may be invalid. Use [`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(&mut self, ctx: &Context<'ph>, f: F) -> Result> 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> {
/// 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, 'ph: 'f {
assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks");
unsafe extern "C" fn callback(word: *const *const c_char, word_eol: *const *const c_char, ud: *mut c_void) -> c_int {
let f: Rc = rc_clone_from_raw(ud as *const HookUd);
(f)(word, word_eol, ptr::null()).do_eat as c_int
}
let b: Rc = {
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> {
/// 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, 'ph: 'f {
assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks");
self.hook_server_attrs(cmd, pri, move |ph, w, we, _| cb(ph, w, we))
}
/// Sets a server hook, with attributes.
pub fn hook_server_attrs<'f, F>(&self, cmd: &str, pri: i32, cb: F) -> HookHandle<'ph, 'f> where F: Fn(&mut PluginHandle<'ph>, Word, WordEol, EventAttrs) -> Eat + 'f + RefUnwindSafe, 'ph: 'f {
assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks");
unsafe extern "C" fn callback(word: *const *const c_char, word_eol: *const *const c_char, attrs: *const RawAttrs, ud: *mut c_void) -> c_int {
let f: Rc = rc_clone_from_raw(ud as *const HookUd);
(f)(word, word_eol, attrs).do_eat as c_int
}
let b: Rc = {
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> {
/// 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, 'ph: 'f {
assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks");
// hmm, is there any way to avoid this code duplication?
// hook_print is special because dummy prints (keypresses, Close Context) are handled
// through here, but never through hook_print_attrs. :/
unsafe extern "C" fn callback(word: *const *const c_char, ud: *mut c_void) -> c_int {
let f: Rc = rc_clone_from_raw(ud as *const HookUd);
(f)(word, ptr::null(), ptr::null()).do_eat as c_int
}
let b: Rc = {
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> {
/// 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, 'ph: 'f {
assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks");
unsafe extern "C" fn callback(word: *const *const c_char, attrs: *const RawAttrs, ud: *mut c_void) -> c_int {
let f: Rc = rc_clone_from_raw(ud as *const HookUd);
(f)(word, ptr::null(), attrs).do_eat as c_int
}
let b: Rc = {
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> {
/// 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, 'ph: 'f {
assert_eq!(self.data.ph, self.plugin, "PluginEntryHandle can't have hooks");
unsafe extern "C" fn callback(ud: *mut c_void) -> c_int {
let f: Rc = 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 = {
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(&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> {
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 {
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> {
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> {
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- >(&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
= 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.
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()))
}
}
pub fn emit_print<'b, I: IntoIterator- >(self, event: &str, args: I) -> bool {
let ph = self.ph;
let event = CString::new(event).unwrap();
let mut args_cs: [Option
; 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
}
}
pub fn emit_print_attrs<'b, I: IntoIterator- >(self, attrs: EventAttrs, event: &str, args: I) -> bool {
let ph = self.ph;
let event = CString::new(event).unwrap();
let mut args_cs: [Option
; 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(&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, 'ph: 'f {
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, 'ph: 'f {
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, 'ph: 'f {
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, 'ph: 'f {
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, 'ph: 'f {
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, 'ph: 'f {
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> {
self.ph.get_info(id)
}
/// Returns the plugin directory.
///
/// See [`PluginHandle::get_libdirfs`].
pub fn get_libdirfs(&self) -> Option {
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 Eat + ::std::panic::RefUnwindSafe>;
// /// Userdata type used by a server hook.
// type ServerHookUd = Box Eat + ::std::panic::RefUnwindSafe>;
// /// Userdata type used by a print hook.
// type PrintHookUd = Box Eat + ::std::panic::RefUnwindSafe>;
// /// Userdata type used by a timer hook.
// type TimerHookUd = Box bool + ::std::panic::RefUnwindSafe>;
/// Userdata type used by a hook
type HookUd<'f> = Box Eat + RefUnwindSafe + 'f>;
/// Contexts
type Contexts = Rc>>>>;
/// 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(ptr: *const T) -> Rc {
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 + '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>) {
(*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> {
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>> = {
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(_) = r {
// TODO try to log panic?
}
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 = None;
{
let mut ausinfo = AssertUnwindSafe(&mut info);
safe_to_unload = if 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.
}).is_ok() { 1 } else { 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.
};
}
|