/*
* Hexchat Plugin API Bindings for Rust
* Copyright (C) 2018 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
//!
//! ```
//! #[macro_use]
//! extern crate hexchat_plugin;
//!
//! use hexchat_plugin::{Plugin, PluginHandle};
//!
//! #[derive(Default)]
//! struct MyPlugin {}
//!
//! impl Plugin for MyPlugin {
//! fn init(&self, ph: &mut PluginHandle, arg: Option<&str>) -> bool {
//! ph.register("myplugin", "0.1.0", "my simple plugin");
//! ph.hook_command("hello-world", |ph, arg, arg_eol| {
//! ph.print("Hello, World!");
//! hexchat_plugin::EAT_ALL
//! }, 0, Some("prints 'Hello, World!'"));
//! true
//! }
//! }
//!
//! hexchat_plugin!(MyPlugin);
//!
//! # 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.
*/
/*
* 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
* -[#] 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.)
* -[ ] hexchat_emit_print
* -[ ] hexchat_emit_print_attrs
* -[ ] hexchat_send_modes
* -[ ] hexchat_nickcmp
* -[ ] hexchat_strip
* -[x] ~~hexchat_free~~ not available - use Drop impls.
* -[x] ~~hexchat_event_attrs_create~~ not available - converted as needed
* -[x] ~~hexchat_event_attrs_free~~ not available - use Drop impls.
* -[x] hexchat_get_info
* -[ ] hexchat_get_prefs
* -[ ] hexchat_list_get, hexchat_list_fields, hexchat_list_next, hexchat_list_str,
* hexchat_list_int, hexchat_list_time, hexchat_list_free
* -[x] hexchat_hook_command
* -[ ] hexchat_hook_fd
* -[#] hexchat_hook_print (implemented through _attrs)
* -[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
* -[ ] hexchat_plugingui_add
* -[x] ~~hexchat_plugingui_remove~~ not available - use Drop impls.
* -[ ] Wrap contexts within something we keep track of. Mark them invalid when contexts are
* closed. [PRI-MAYBE]
* -[x] 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 internals;
use std::borrow::Cow;
use std::cell::Cell;
use std::ffi::{CString, CStr};
use std::marker::PhantomData;
use std::mem;
use std::ops;
use std::panic::catch_unwind;
use std::ptr;
use std::rc::Rc;
use std::str::FromStr;
use std::thread;
use std::time::{SystemTime, UNIX_EPOCH, Duration};
// ****** //
// PUBLIC //
// ****** //
// Consts
// EAT_*
/// Equivalent to HEXCHAT_EAT_NONE.
pub const EAT_NONE: Eat = Eat { do_eat: 0 };
/// Equivalent to HEXCHAT_EAT_HEXCHAT.
pub const EAT_HEXCHAT: Eat = Eat { do_eat: 1 };
/// Equivalent to HEXCHAT_EAT_PLUGIN.
pub const EAT_PLUGIN: Eat = Eat { do_eat: 2 };
/// Equivalent to HEXCHAT_EAT_ALL.
pub const EAT_ALL: Eat = Eat { do_eat: 1 | 2 };
// 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.
pub trait Plugin {
/// Called to initialize the plugin.
fn init(&self, ph: &mut PluginHandle, arg: Option<&str>) -> bool;
/// Called to deinitialize the plugin.
///
/// This is always called immediately prior to Drop::drop.
fn deinit(&self, _ph: &mut PluginHandle) {
}
}
// Structs
/// A hexchat plugin handle, used to register hooks and interact with hexchat.
///
/// # Examples
///
/// ```
/// use hexchat_plugin::{PluginHandle};
///
/// fn init(ph: &mut PluginHandle) {
/// ph.register("myplug", "0.1.0", "my awesome plug");
/// }
/// ```
pub struct PluginHandle {
ph: *mut internals::Ph,
// Used for registration
info: PluginInfo,
}
/// Arguments passed to a hook, until the next argument.
///
/// # Examples
///
/// ```
/// use hexchat_plugin::{PluginHandle, Word, WordEol, Eat};
///
/// fn cmd_foo(ph: &mut PluginHandle, arg: Word, arg_eol: WordEol) -> Eat {
/// if arg.len() < 3 {
/// ph.print("Not enough arguments");
/// } else {
/// ph.print(&format!("{} {} {}", arg[0], arg[1], arg[2]));
/// }
/// hexchat_plugin::EAT_ALL
/// }
///
/// fn on_privmsg(ph: &mut PluginHandle, word: Word, word_eol: WordEol) -> Eat {
/// if word.len() > 0 && word[0].starts_with('@') {
/// ph.print("We have message tags!?");
/// }
/// hexchat_plugin::EAT_NONE
/// }
/// ```
pub struct Word<'a> {
word: Vec<&'a str>
}
/// Arguments passed to a hook, until the end of the line.
///
/// # Examples
///
/// ```
/// use hexchat_plugin::{PluginHandle, Word, WordEol, Eat};
///
/// fn cmd_foo(ph: &mut PluginHandle, arg: Word, arg_eol: WordEol) -> Eat {
/// if arg.len() < 3 {
/// ph.print("Not enough arguments");
/// } else {
/// ph.print(&format!("{} {} {}", arg[0], arg[1], arg_eol[2]));
/// }
/// hexchat_plugin::EAT_ALL
/// }
///
/// fn on_privmsg(ph: &mut PluginHandle, word: Word, word_eol: WordEol) -> Eat {
/// if word_eol.len() > 0 && word[0].starts_with('@') {
/// ph.print("We have message tags!?");
/// }
/// hexchat_plugin::EAT_NONE
/// }
/// ```
pub struct WordEol<'a> {
word_eol: Vec<&'a str>
}
/// A safety wrapper to ensure you're working with a valid context.
///
/// This mechanism attempts to reduce the likelihood of segfaults.
pub struct EnsureValidContext<'a> {
ph: &'a mut PluginHandle,
}
/// Event attributes.
#[derive(Clone)]
pub struct EventAttrs<'a> {
/// Server time.
pub server_time: Option,
_dummy: PhantomData<&'a ()>,
}
/// A status indicator for event callbacks. Indicates whether to continue processing, eat hexchat,
/// eat plugin, or eat all.
///
/// Returned by most hooks.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Eat {
do_eat: i32,
}
/// A command hook handle.
pub struct CommandHookHandle {
ph: *mut internals::Ph,
hh: *const internals::HexchatHook,
// this does actually store an Rc<...>, but on the other side of the FFI.
_f: PhantomData>,
}
/// A server hook handle.
pub struct ServerHookHandle {
ph: *mut internals::Ph,
hh: *const internals::HexchatHook,
// this does actually store an Rc<...>, but on the other side of the FFI.
_f: PhantomData>,
}
/// A print hook handle.
pub struct PrintHookHandle {
ph: *mut internals::Ph,
hh: *const internals::HexchatHook,
// this does actually store an Rc<...>, but on the other side of the FFI.
_f: PhantomData>,
}
/// A timer hook handle.
pub struct TimerHookHandle {
ph: *mut internals::Ph,
hh: *const internals::HexchatHook,
// avoids issues
alive: Rc>,
// this does actually store an Rc<...>, but on the other side of the FFI.
_f: PhantomData>,
}
/// A context.
// We don't want this Copy + Clone, as we may want to implement a context invalidation system
// at some point (rather than relying on the hexchat allocator not to allocate a new context where
// a context was free'd).
pub struct Context {
ctx: *const internals::HexchatContext,
}
// #[derive(Debug)] // doesn't work
pub struct InvalidContextError R, R>(F);
// Enums
/// A hexchat_get_info key.
#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Copy, Clone)]
pub enum InfoId<'a> {
/// Returns the away message, or `None` if the user is not away.
Away,
/// Returns the current channel name.
Channel,
/// Returns the current charset.
Charset,
/// Returns the hexchat configuration directory, e.g. `/home/user/.config/hexchat`.
Configdir,
/// Returns the text event format string for the given text event name.
EventText(&'a str),
/// Returns the (real) hostname of the current server.
Host,
/// Returns the contents of the input box.
Inputbox,
/// Returns the library directory, e.g. `/usr/lib/hexchat`.
///
/// May not always work, as this string isn't necessarily UTF-8, but local file system
/// encoding.
Libdirfs,
/// Returns the channel modes, if known, or `None`.
Modes,
/// Returns the current network name, or `None`.
Network,
/// Returns the user's current nick.
Nick,
/// Returns the user's nickserv password, if any, or `None`
Nickserv,
/// Returns the current server name, or `None` if you are not connected.
Server,
/// Returns the current channel topic.
Topic,
/// Returns the HexChat version string.
Version,
/// Returns the window status: "active", "hidden" or "normal".
WinStatus,
}
// ***** //
// impls //
// ***** //
impl<'a> InfoId<'a> {
pub fn name(&self) -> Cow<'static, str> {
match *self {
InfoId::EventText(s) => {
let mut eventtext: String = "event_text ".into();
eventtext.push_str(&s);
eventtext.into()
},
InfoId::Away => "away".into(),
InfoId::Channel => "channel".into(),
InfoId::Charset => "charset".into(),
InfoId::Configdir => "configdir".into(),
InfoId::Host => "host".into(),
InfoId::Inputbox => "inputbox".into(),
InfoId::Libdirfs => "libdirfs".into(),
InfoId::Modes => "modes".into(),
InfoId::Network => "network".into(),
InfoId::Nick => "nick".into(),
InfoId::Nickserv => "nickserv".into(),
InfoId::Server => "server".into(),
InfoId::Topic => "topic".into(),
InfoId::Version => "version".into(),
InfoId::WinStatus => "win_status".into(),
}
}
}
impl R, R> InvalidContextError {
pub fn get_closure(self) -> F {
self.0
}
}
impl Drop for CommandHookHandle {
fn drop(&mut self) {
unsafe {
let b = ((*self.ph).hexchat_unhook)(self.ph, self.hh) as *mut CommandHookUd;
// we assume b is not null. this should be safe.
// just drop it
mem::drop(Rc::from_raw(b));
}
}
}
impl Drop for ServerHookHandle {
fn drop(&mut self) {
unsafe {
let b = ((*self.ph).hexchat_unhook)(self.ph, self.hh) as *mut ServerHookUd;
// we assume b is not null. this should be safe.
// just drop it
mem::drop(Rc::from_raw(b));
}
}
}
impl Drop for PrintHookHandle {
fn drop(&mut self) {
unsafe {
let b = ((*self.ph).hexchat_unhook)(self.ph, self.hh) as *mut PrintHookUd;
// we assume b is not null. this should be safe.
// just drop it
mem::drop(Rc::from_raw(b));
}
}
}
impl Drop for TimerHookHandle {
fn drop(&mut self) {
if !self.alive.get() {
// already free'd.
return;
}
self.alive.set(false);
unsafe {
let b = ((*self.ph).hexchat_unhook)(self.ph, self.hh) as *mut TimerHookUd;
// we assume b is not null. this should be safe.
// just drop it
mem::drop(Rc::from_raw(b));
}
}
}
impl<'a> Word<'a> {
unsafe fn new(word: *const *const libc::c_char) -> Word<'a> {
let mut vec = vec![];
for i in 1..32 {
let w = *word.offset(i);
if !w.is_null() {
vec.push(CStr::from_ptr(w).to_str().expect("non-utf8 word - broken hexchat"))
}
}
while let Some(&"") = vec.last() {
vec.pop();
}
Word { word: vec }
}
}
impl<'a> ops::Deref for Word<'a> {
type Target = [&'a str];
fn deref(&self) -> &[&'a str] {
&self.word
}
}
impl<'a> WordEol<'a> {
unsafe fn new(word_eol: *const *const libc::c_char) -> WordEol<'a> {
let mut vec = vec![];
for i in 1..32 {
let w = *word_eol.offset(i);
if !w.is_null() {
vec.push(CStr::from_ptr(w).to_str().expect("non-utf8 word_eol - broken hexchat"))
}
}
while let Some(&"") = vec.last() {
vec.pop();
}
WordEol { word_eol: vec }
}
}
impl<'a> ops::Deref for WordEol<'a> {
type Target = [&'a str];
fn deref(&self) -> &[&'a str] {
&self.word_eol
}
}
impl PluginHandle {
fn new(ph: *mut internals::Ph, info: PluginInfo) -> PluginHandle {
PluginHandle {
ph, info
}
}
/// 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
///
/// ```
/// use hexchat_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) {
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 {
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 {
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 {
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.
// NOTE: using a closure is nicer.
// TODO check if this is actually safe
pub fn ensure_valid_context(&mut self, f: F) -> R where F: FnOnce(EnsureValidContext) -> R {
// let ctx = self.get_context();
// if !self.set_context(ctx) {
// // invalid context
// // TODO fix up the context
// unimplemented!()
// }
// f(EnsureValidContext { ph: self })
let ctx = self.get_context();
// need this here because we don't have NLL yet
let res = self.with_context(&ctx, f);
match res {
Result::Ok(r @ _) => r,
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?")
}
}
}
/// Returns the current context.
///
/// Note: The returned context may be invalid. Use [`set_context`] to check.
///
/// [`set_context`]: #method.set_context
pub fn get_context(&mut self) -> Context {
unsafe {
Context { ctx: ((*self.ph).hexchat_get_context)(self.ph) }
}
}
/// Sets the current context.
///
/// Returns `true` if the context is valid, `false` otherwise.
pub fn set_context(&mut self, ctx: &Context) -> bool {
unsafe {
((*self.ph).hexchat_set_context)(self.ph, ctx.ctx) != 0
}
}
/// Do something in a valid context.
///
/// Note that this changes the active context and doesn't change it back.
///
/// # Errors
///
/// Returns `Err(InvalidContextError(f))` if the context is invalid. See [`set_context`]. Otherwise,
/// calls `f` and returns `Ok(its result)`.
///
/// Note that `InvalidContextError` contains the original closure. This allows you to retry.
///
/// [`set_context`]: #method.set_context
// this is probably safe to inline, and actually a good idea for ensure_valid_context
#[inline]
pub fn with_context(&mut self, ctx: &Context, f: F) -> Result> where F: FnOnce(EnsureValidContext) -> R {
if !self.set_context(ctx) {
Err(InvalidContextError(f))
} else {
Ok(f(EnsureValidContext { ph: self }))
}
}
/// Sets a command hook.
///
/// # Examples
///
/// ```
/// use hexchat_plugin::PluginHandle;
///
/// fn register_commands(ph: &mut PluginHandle) {
/// ph.hook_command("hello-world", |ph, arg, arg_eol| {
/// ph.print("Hello, World!");
/// hexchat_plugin::EAT_ALL
/// }, 0, Some("prints 'Hello, World!'"));
/// }
/// ```
pub fn hook_command(&mut self, cmd: &str, cb: F, pri: i32, help: Option<&str>) -> CommandHookHandle where F: Fn(&mut PluginHandle, Word, WordEol) -> Eat + 'static + ::std::panic::RefUnwindSafe {
unsafe extern "C" fn callback(word: *const *const libc::c_char, word_eol: *const *const libc::c_char, ud: *mut libc::c_void) -> libc::c_int {
// hook may unhook itself.
// however, we don't wanna free it until it has returned.
let f: Rc = rc_clone_from_raw(ud as *const CommandHookUd);
let ph = f.1;
match catch_unwind(move || {
let word = Word::new(word);
let word_eol = WordEol::new(word_eol);
(f.0)(&mut PluginHandle::new(f.1, f.2), word, word_eol).do_eat as libc::c_int
}) {
Result::Ok(v @ _) => v,
Result::Err(e @ _) => {
// if it's a &str or String, just print it
if let Some(estr) = e.downcast_ref::<&str>() {
hexchat_print_str(ph, estr, false);
} else if let Some(estring) = e.downcast_ref::() {
hexchat_print_str(ph, &estring, false);
}
0 // EAT_NONE
}
}
}
let b: Rc = Rc::new((Box::new(cb), self.ph, self.info));
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 = ((*self.ph).hexchat_hook_command)(self.ph, name.as_ptr(), pri as libc::c_int, callback, help_text.as_ref().map(|s| s.as_ptr()).unwrap_or(ptr::null()), bp as *mut _);
assert!(!res.is_null());
CommandHookHandle { ph: self.ph, hh: res, _f: PhantomData }
}
}
/// Sets a server hook.
///
/// # Examples
///
/// ```
/// use hexchat_plugin::PluginHandle;
///
/// fn register_server_hooks(ph: &mut PluginHandle) {
/// ph.hook_server("PRIVMSG", |ph, word, word_eol| {
/// if word.len() > 0 && word[0].starts_with('@') {
/// ph.print("We have message tags!?");
/// }
/// hexchat_plugin::EAT_NONE
/// }, 0);
/// }
/// ```
pub fn hook_server(&mut self, cmd: &str, cb: F, pri: i32) -> ServerHookHandle where F: Fn(&mut PluginHandle, Word, WordEol) -> Eat + 'static + ::std::panic::RefUnwindSafe {
self.hook_server_attrs(cmd, move |ph, w, we, _| cb(ph, w, we), pri)
}
/// Sets a server hook, with attributes.
pub fn hook_server_attrs(&mut self, cmd: &str, cb: F, pri: i32) -> ServerHookHandle where F: Fn(&mut PluginHandle, Word, WordEol, EventAttrs) -> Eat + 'static + ::std::panic::RefUnwindSafe {
unsafe extern "C" fn callback(word: *const *const libc::c_char, word_eol: *const *const libc::c_char, attrs: *const internals::HexchatEventAttrs, ud: *mut libc::c_void) -> libc::c_int {
// hook may unhook itself.
// however, we don't wanna free it until it has returned.
let f: Rc = rc_clone_from_raw(ud as *const ServerHookUd);
let ph = f.1;
match catch_unwind(move || {
let word = Word::new(word);
let word_eol = WordEol::new(word_eol);
(f.0)(&mut PluginHandle::new(f.1, f.2), word, word_eol, (&*attrs).into()).do_eat as libc::c_int
}) {
Result::Ok(v @ _) => v,
Result::Err(e @ _) => {
// if it's a &str or String, just print it
if let Some(estr) = e.downcast_ref::<&str>() {
hexchat_print_str(ph, estr, false);
} else if let Some(estring) = e.downcast_ref::() {
hexchat_print_str(ph, &estring, false);
}
0 // EAT_NONE
}
}
}
let b: Rc = Rc::new((Box::new(cb), self.ph, self.info));
let name = CString::new(cmd).unwrap();
let bp = Rc::into_raw(b);
unsafe {
let res = ((*self.ph).hexchat_hook_server_attrs)(self.ph, name.as_ptr(), pri as libc::c_int, callback, bp as *mut _);
assert!(!res.is_null());
ServerHookHandle { ph: self.ph, hh: res, _f: PhantomData }
}
}
/// Sets a print hook.
///
/// # Examples
///
/// ```
/// use hexchat_plugin::PluginHandle;
///
/// fn register_print_hooks(ph: &mut PluginHandle) {
/// ph.hook_print("Channel Message", |ph, arg| {
/// if let Some(nick) = arg.get(0) {
/// if *nick == "KnOwN_SpAmMeR" {
/// return hexchat_plugin::EAT_ALL
/// }
/// }
/// hexchat_plugin::EAT_NONE
/// }, 0);
/// }
/// ```
pub fn hook_print(&mut self, name: &str, cb: F, pri: i32) -> PrintHookHandle where F: Fn(&mut PluginHandle, Word) -> Eat + 'static + ::std::panic::RefUnwindSafe {
self.hook_print_attrs(name, move |ph, w, _| cb(ph, w), pri)
}
/// Sets a print hook, with attributes.
pub fn hook_print_attrs(&mut self, name: &str, cb: F, pri: i32) -> PrintHookHandle where F: Fn(&mut PluginHandle, Word, EventAttrs) -> Eat + 'static + ::std::panic::RefUnwindSafe {
unsafe extern "C" fn callback(word: *const *const libc::c_char, attrs: *const internals::HexchatEventAttrs, ud: *mut libc::c_void) -> libc::c_int {
// hook may unhook itself.
// however, we don't wanna free it until it has returned.
let f: Rc = rc_clone_from_raw(ud as *const PrintHookUd);
let ph = f.1;
match catch_unwind(move || {
let word = Word::new(word);
(f.0)(&mut PluginHandle::new(f.1, f.2), word, (&*attrs).into()).do_eat as libc::c_int
}) {
Result::Ok(v @ _) => v,
Result::Err(e @ _) => {
// if it's a &str or String, just print it
if let Some(estr) = e.downcast_ref::<&str>() {
hexchat_print_str(ph, estr, false);
} else if let Some(estring) = e.downcast_ref::() {
hexchat_print_str(ph, &estring, false);
}
0 // EAT_NONE
}
}
}
let b: Rc = Rc::new((Box::new(cb), self.ph, self.info));
let name = CString::new(name).unwrap();
let bp = Rc::into_raw(b);
unsafe {
let res = ((*self.ph).hexchat_hook_print_attrs)(self.ph, name.as_ptr(), pri as libc::c_int, callback, bp as *mut _);
assert!(!res.is_null());
PrintHookHandle { ph: self.ph, hh: res, _f: PhantomData }
}
}
/// Sets a timer hook
///
/// # Examples
///
/// ```
/// use hexchat_plugin::PluginHandle;
///
/// fn register_timers(ph: &mut PluginHandle) {
/// ph.hook_timer(2000, |ph| {
/// ph.print("timer up!");
/// false
/// });
/// }
/// ```
pub fn hook_timer(&mut self, timeout: i32, cb: F) -> TimerHookHandle where F: Fn(&mut PluginHandle) -> bool + 'static + ::std::panic::RefUnwindSafe {
unsafe extern "C" fn callback(ud: *mut libc::c_void) -> libc::c_int {
// hook may unhook itself.
// however, we don't wanna free it until it has returned.
let f: Rc = rc_clone_from_raw(ud as *const TimerHookUd);
let alive = f.1.clone(); // clone the alive because why not
let f = f.0.clone();
let ph = f.1;
// we could technically free the Rc here, I guess
match catch_unwind(move || {
(f.0)(&mut PluginHandle::new(f.1, f.2))
}) {
Result::Ok(true) => 1,
Result::Ok(false) => {
// avoid double-free
if !alive.get() {
return 0;
}
// mark it no longer alive
alive.set(false);
// HexChat will automatically free the hook.
// we just need to free the userdata.
mem::drop(Rc::from_raw(ud as *const TimerHookUd));
0
},
Result::Err(e @ _) => {
// if it's a &str or String, just print it
if let Some(estr) = e.downcast_ref::<&str>() {
hexchat_print_str(ph, estr, false);
} else if let Some(estring) = e.downcast_ref::() {
hexchat_print_str(ph, &estring, false);
}
// avoid double-free
if !alive.get() {
return 0;
}
// mark it no longer alive
alive.set(false);
// HexChat will automatically free the hook.
// we just need to free the userdata.
mem::drop(Rc::from_raw(ud as *const TimerHookUd));
0
}
}
}
let alive = Rc::new(Cell::new(true));
let b: Rc = Rc::new((Rc::new((Box::new(cb), self.ph, self.info)), alive.clone()));
let bp = Rc::into_raw(b);
unsafe {
let res = ((*self.ph).hexchat_hook_timer)(self.ph, timeout as libc::c_int, callback, bp as *mut _);
assert!(!res.is_null());
TimerHookHandle { ph: self.ph, hh: res, alive, _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(&mut self, s: &str) {
unsafe {
hexchat_print_str(self.ph, s, true);
}
}
/// Returns information on the current context.
///
/// Note: `InfoId::Libdirfs` may return `None` or broken results if the result wouldn't be (valid) UTF-8.
// TODO this should be `id: InfoId`. fix in 0.3
pub fn get_info(&mut self, id: &InfoId) -> Option {
let ph = self.ph;
let id_cstring = CString::new(&*id.name()).unwrap();
unsafe {
let res = ((*ph).hexchat_get_info)(ph, id_cstring.as_ptr());
if res.is_null() {
None
} else {
let s = CStr::from_ptr(res).to_owned().into_string();
if *id != InfoId::Libdirfs {
Some(s.expect("non-utf8 word - broken hexchat"))
} else {
s.ok()
}
}
}
}
// ******* //
// PRIVATE //
// ******* //
fn find_valid_context(&mut self) -> Option {
unsafe {
let ph = self.ph;
// TODO wrap this in a safer API, with proper Drop
#[allow(unused_mut)]
let mut list = ((*ph).hexchat_list_get)(ph, cstr(b"channels\0"));
// hexchat does this thing where it puts a context in a list_str.
// this is the proper way to do this
if ((*ph).hexchat_list_next)(ph, list) != 0 {
// if this panics we may leak some memory. it's not a big deal tho, as it indicates
// a bug in hexchat-plugin.rs.
let ctx = ((*ph).hexchat_list_str)(ph, list, cstr(b"context\0")) as *const internals::HexchatContext;
((*ph).hexchat_list_free)(ph, list);
Some(Context { ctx })
} else {
((*ph).hexchat_list_free)(ph, list);
None
}
}
}
}
impl<'a> EventAttrs<'a> {
fn new() -> EventAttrs<'a> {
EventAttrs {
server_time: None,
_dummy: PhantomData,
}
}
}
impl<'a> From<&'a internals::HexchatEventAttrs> for EventAttrs<'a> {
fn from(other: &'a internals::HexchatEventAttrs) -> 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> EnsureValidContext<'a> {
/*
* 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 EnsureValidContext - things that don't invalidate the
* context take an `&mut self`, things that do take an `self`.
*/
/// Finds an open context for the given servname and channel.
pub fn find_context(&mut self, servname: Option<&str>, channel: Option<&str>) -> Option {
// this was a mistake but oh well
let ph = self.ph.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).hexchat_find_context)(ph, sptr, cptr)
};
if ctx.is_null() {
None
} else {
Some(Context { ctx })
}
}
/// Compares two nicks based on the server's case mapping.
pub fn nickcmp(&mut self, nick1: &str, nick2: &str) -> ::std::cmp::Ordering {
use std::cmp::Ordering;
// this was a mistake but oh well
let ph = self.ph.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).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) {
// this was a mistake but oh well
let ph = self.ph.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 libc::c_char> = (&v).iter().map(|x| x.as_ptr()).collect();
let arr: &mut [*const libc::c_char] = &mut *v2;
unsafe {
((*ph).hexchat_send_modes)(ph, arr.as_mut_ptr(), arr.len() as libc::c_int,
mpl as libc::c_int, sign as libc::c_char, mode as libc::c_char)
}
}
/// Executes a command.
pub fn command(self, cmd: &str) {
// this was a mistake but oh well
let ph = self.ph.ph;
// need to put this in a more permanent position than temporaries
let cmd = CString::new(cmd).unwrap();
unsafe {
((*ph).hexchat_command)(ph, cmd.as_ptr())
}
}
pub fn emit_print(self) {
// TODO
unimplemented!()
}
pub fn emit_print_attrs(self) {
// TODO
unimplemented!()
}
// ******** //
// FORWARDS //
// ******** //
// We can't just deref because then you could recursively ensure valid context and then it'd no
// longer work.
pub fn get_context(&mut self) -> Context {
self.ph.get_context()
}
/// Sets the current context.
///
/// Returns `true` if the context is valid, `false` otherwise.
pub fn set_context(&mut self, ctx: &Context) -> bool {
self.ph.set_context(ctx)
}
/// Prints to the hexchat buffer.
// as of hexchat 2.14.1, this does not call hooks.
pub fn print(&mut self, s: &str) {
self.ph.print(s)
}
/// Sets a command hook
pub fn hook_command(&mut self, cmd: &str, cb: F, pri: i32, help: Option<&str>) -> CommandHookHandle where F: Fn(&mut PluginHandle, Word, WordEol) -> Eat + 'static + ::std::panic::RefUnwindSafe {
self.ph.hook_command(cmd, cb, pri, help)
}
/// Sets a server hook
pub fn hook_server(&mut self, cmd: &str, cb: F, pri: i32) -> ServerHookHandle where F: Fn(&mut PluginHandle, Word, WordEol) -> Eat + 'static + ::std::panic::RefUnwindSafe {
self.ph.hook_server(cmd, cb, pri)
}
/// Sets a print hook
pub fn hook_print(&mut self, name: &str, cb: F, pri: i32) -> PrintHookHandle where F: Fn(&mut PluginHandle, Word) -> Eat + 'static + ::std::panic::RefUnwindSafe {
self.ph.hook_print(name, cb, pri)
}
/// Sets a timer hook
pub fn hook_timer(&mut self, timeout: i32, cb: F) -> TimerHookHandle where F: Fn(&mut PluginHandle) -> bool + 'static + ::std::panic::RefUnwindSafe {
self.ph.hook_timer(timeout, cb)
}
pub fn get_info(&mut self, id: &InfoId) -> Option {
self.ph.get_info(id)
}
}
// ******* //
// 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>, *mut internals::Ph, PluginInfo);
/// Userdata type used by a server hook.
type ServerHookUd = (Box Eat + ::std::panic::RefUnwindSafe>, *mut internals::Ph, PluginInfo);
/// Userdata type used by a print hook.
type PrintHookUd = (Box Eat + ::std::panic::RefUnwindSafe>, *mut internals::Ph, PluginInfo);
/// Userdata type used by a timer hook.
type TimerHookUd = (Rc<(Box bool + ::std::panic::RefUnwindSafe>, *mut internals::Ph, PluginInfo)>, 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 libc::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 {
// this is a bit confusing to read, but basically, we get an Rc from the raw ptr, and increment
// the refcount.
// The construct mem::forget(rc.clone()) increments the refcount.
let rc = Rc::from_raw(ptr);
mem::forget(rc.clone());
rc
}
/// 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 internals::Ph, 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 internals::HexchatEventAttrs
struct HexchatEventAttrsHelper(*mut internals::HexchatEventAttrs, *mut internals::Ph);
impl HexchatEventAttrsHelper {
fn new(ph: *mut internals::Ph) -> Self {
HexchatEventAttrsHelper(unsafe { ((*ph).hexchat_event_attrs_create)(ph) }, ph)
}
fn new_with(ph: *mut internals::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 < (libc::time_t::max_value() as u64)).unwrap() as libc::time_t;
unsafe { (*helper.0).server_time_utc = v; }
helper
}
}
impl Drop for HexchatEventAttrsHelper {
fn drop(&mut self) {
unsafe {
((*self.1).hexchat_event_attrs_free)(self.1, self.0)
}
}
}
/// Holds name, desc, vers
// This is kinda naughty - we modify these values after returning from hexchat_plugin_init, during
// the deinitialization.
// However, if my reading of the HexChat code is correct, this is "ok".
#[derive(Copy, Clone)]
struct PluginInfo {
name: *mut *const libc::c_char,
desc: *mut *const libc::c_char,
vers: *mut *const libc::c_char,
}
impl PluginInfo {
/// Creates a PluginInfo.
///
/// # Panics
///
/// This function explicitly doesn't panic. Call unwrap() on the result instead.
fn new(name: *mut *const libc::c_char, desc: *mut *const libc::c_char, vers: *mut *const libc::c_char) -> Option {
if name.is_null() || desc.is_null() || vers.is_null() || name == desc || desc == vers || name == vers {
None
} else {
Some(unsafe { PluginInfo::new_unchecked(name, desc, vers) })
}
}
/// Creates a PluginInfo without checking the arguments.
///
/// # Safety
///
/// This function is unsafe, as it doesn't check the validity of the arguments. You're expected
/// to only pass in non-aliased non-null pointers. Use new if unsure.
unsafe fn new_unchecked(name: *mut *const libc::c_char, desc: *mut *const libc::c_char, vers: *mut *const libc::c_char) -> PluginInfo {
PluginInfo {
name, desc, vers
}
}
/// Drop relevant CStrings.
///
/// # Safety
///
/// This function is unsafe because calling it may trigger Undefined Behaviour. See also
/// [CString::from_raw].
///
/// [from_raw]: https://doc.rust-lang.org/std/ffi/struct.CString.html#method.from_raw
unsafe fn drop_info(&mut self) {
if !(*self.name).is_null() {
mem::drop(CString::from_raw(*self.name as *mut _));
*self.name = cstr(EMPTY_CSTRING_DATA);
}
if !(*self.desc).is_null() {
mem::drop(CString::from_raw(*self.desc as *mut _));
*self.desc = cstr(EMPTY_CSTRING_DATA);
}
if !(*self.vers).is_null() {
mem::drop(CString::from_raw(*self.vers as *mut _));
*self.vers = cstr(EMPTY_CSTRING_DATA);
}
}
}
/// Plugin data stored in the hexchat plugin_handle.
struct PhUserdata {
plug: Box,
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: *mut internals::Ph, ud: Box) {
(*ph).userdata = Box::into_raw(ud) as *mut libc::c_void;
}
// unsafe fn get_userdata(ph: *mut internals::Ph) -> *const PhUserdata {
// (*ph).userdata as *const _
// }
/// 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: *mut internals::Ph) -> Box {
Box::from_raw(mem::replace(&mut (*ph).userdata, ptr::null_mut()) as *mut PhUserdata)
}
// *********************** //
// PUBLIC OUT OF NECESSITY //
// *********************** //
#[doc(hidden)]
pub unsafe fn hexchat_plugin_init(plugin_handle: *mut libc::c_void,
plugin_name: *mut *const libc::c_char,
plugin_desc: *mut *const libc::c_char,
plugin_version: *mut *const libc::c_char,
arg: *const libc::c_char) -> libc::c_int
where T: Plugin + Default + 'static {
if plugin_handle.is_null() || plugin_name.is_null() || plugin_desc.is_null() || plugin_version.is_null() {
// we can't really do anything here.
eprintln!("hexchat_plugin_init called with a null pointer that shouldn't be null - broken hexchat");
// TODO maybe call abort.
return 0;
}
let ph = plugin_handle as *mut internals::Ph;
// 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