Version 0.2.1

This commit is contained in:
SoniEx2 2018-03-31 11:31:52 -03:00
parent 6ce60768df
commit b2be734b53
3 changed files with 294 additions and 21 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "hexchat-plugin"
version = "0.2.0"
version = "0.2.1"
authors = ["SoniEx2 <endermoneymod@gmail.com>"]
description = "Lets you write HexChat plugins in Rust"
license = "AGPL-3.0+"

View File

@ -66,18 +66,18 @@ pub struct Ph {
name: *const libc::c_char,
pri: libc::c_int,
/* CALLBACK */
callback: Option<unsafe extern "C" fn(word: *const *const libc::c_char, word_eol: *const *const libc::c_char, user_data: *mut libc::c_void) -> libc::c_int>,
callback: unsafe extern "C" fn(word: *const *const libc::c_char, word_eol: *const *const libc::c_char, user_data: *mut libc::c_void) -> libc::c_int,
userdata: *mut libc::c_void) -> *const HexchatHook,
pub hexchat_hook_print: unsafe extern "C" fn(ph: *mut HexchatPlugin,
name: *const libc::c_char,
pri: libc::c_int,
/* CALLBACK */
callback: Option<unsafe extern "C" fn(word: *const *const libc::c_char, user_data: *mut libc::c_void) -> libc::c_int>,
callback: unsafe extern "C" fn(word: *const *const libc::c_char, user_data: *mut libc::c_void) -> libc::c_int,
userdata: *mut libc::c_void) -> *const HexchatHook,
pub hexchat_hook_timer: unsafe extern "C" fn(ph: *mut HexchatPlugin,
timeout: libc::c_int,
/* CALLBACK */
callback: Option<unsafe extern "C" fn(user_data: *mut libc::c_void) -> libc::c_int>,
callback: unsafe extern "C" fn(user_data: *mut libc::c_void) -> libc::c_int,
userdata: *mut libc::c_void) -> *const HexchatHook,
pub hexchat_hook_fd: unsafe extern "C" fn(ph: *mut HexchatPlugin,
fd: libc::c_int,

View File

@ -67,9 +67,50 @@
/*
* Big list o' TODO:
* -[ ] Finish basic API support. [PRI-HIGH]
* -[ ] 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.
* -[ ] hexchat_event_attrs_create
* -[x] ~~hexchat_event_attrs_free~~ not available - use Drop impls.
* -[ ] 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
* -[x] hexchat_hook_print
* -[ ] hexchat_hook_print_attrs
* -[x] hexchat_hook_server
* -[ ] hexchat_hook_server_attrs
* -[x] hexchat_hook_timer
* -[x] ~~hexchat_unhook~~ not available - use Drop impls
* -[ ] 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<T>. Many (most?) hooks are reentrant. As far as I
* know, all of them need this.
* -[x] Additionally, use a Cell<bool> for timers.
* -[ ] ???
*/
@ -86,6 +127,8 @@ use std::mem;
use std::ptr;
use std::marker::PhantomData;
use std::ops;
use std::rc::Rc;
use std::cell::Cell;
// ****** //
// PUBLIC //
@ -139,12 +182,38 @@ pub const EAT_PLUGIN: Eat = Eat { do_eat: 2 };
/// Equivalent to HEXCHAT_EAT_ALL.
pub const EAT_ALL: Eat = Eat { do_eat: 1 | 2 };
/// A command hook handle, created with PluginHandle::hook_command.
/// A command hook handle.
pub struct CommandHookHandle {
ph: *mut internals::Ph,
hh: *const internals::HexchatHook,
// this does actually store an Box<...>, but on the other side of the FFI.
_f: PhantomData<Box<CommandHookUd>>,
// this does actually store an Rc<...>, but on the other side of the FFI.
_f: PhantomData<Rc<CommandHookUd>>,
}
/// 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<Rc<ServerHookUd>>,
}
/// 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<Rc<PrintHookUd>>,
}
/// A timer hook handle.
pub struct TimerHookHandle {
ph: *mut internals::Ph,
hh: *const internals::HexchatHook,
// avoids issues
alive: Rc<Cell<bool>>,
// this does actually store an Rc<...>, but on the other side of the FFI.
_f: PhantomData<Rc<TimerHookUd>>,
}
/// A context.
@ -170,7 +239,42 @@ impl Drop for CommandHookHandle {
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(Box::from_raw(b));
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));
}
}
}
@ -182,10 +286,11 @@ impl<'a> Word<'a> {
let w = *word.offset(i);
if !w.is_null() {
vec.push(CStr::from_ptr(w).to_str().expect("non-utf8 word - broken hexchat"))
} else {
break
}
}
while let Some(&"") = vec.last() {
vec.pop();
}
Word { word: vec }
}
}
@ -204,10 +309,11 @@ impl<'a> WordEol<'a> {
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"))
} else {
break
}
}
while let Some(&"") = vec.last() {
vec.pop();
}
WordEol { word_eol: vec }
}
}
@ -315,15 +421,17 @@ impl PluginHandle {
/// Sets a command hook
pub fn hook_command<F>(&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 {
let f: *const CommandHookUd = ud as *const CommandHookUd;
match catch_unwind(|| {
// hook may unhook itself.
// however, we don't wanna free it until it has returned.
let f: Rc<CommandHookUd> = 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
(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 @ _) => {
let ph = (*f).1;
// if it's a &str or String, just print it
if let Some(estr) = e.downcast_ref::<&str>() {
hexchat_print_str(ph, estr, false);
@ -334,16 +442,136 @@ impl PluginHandle {
}
}
}
let b: Box<CommandHookUd> = Box::new((Box::new(cb), self.ph, self.info));
let b: Rc<CommandHookUd> = 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 = Box::into_raw(b);
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
pub fn hook_server<F>(&mut self, cmd: &str, cb: F, pri: i32) -> ServerHookHandle 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<ServerHookUd> = 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).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::<String>() {
hexchat_print_str(ph, &estring, false);
}
0 // EAT_NONE
}
}
}
let b: Rc<ServerHookUd> = 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)(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
pub fn hook_print<F>(&mut self, name: &str, cb: F, pri: i32) -> PrintHookHandle where F: Fn(&mut PluginHandle, Word) -> Eat + 'static + ::std::panic::RefUnwindSafe {
unsafe extern "C" fn callback(word: *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<PrintHookUd> = 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).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::<String>() {
hexchat_print_str(ph, &estring, false);
}
0 // EAT_NONE
}
}
}
let b: Rc<PrintHookUd> = 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)(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
pub fn hook_timer<F>(&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<TimerHookUd> = 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<TimerHookUd> 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::<String>() {
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<TimerHookUd> = 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.
@ -418,6 +646,15 @@ impl<'a> EnsureValidContext<'a> {
unimplemented!()
}
/// Executes a command.
pub fn command(self, cmd: &str) {
// this was a mistake but oh well
let ph = self.ph.ph;
unsafe {
((*ph).hexchat_command)(ph, CString::new(cmd).unwrap().as_ptr())
}
}
pub fn emit_print(self) {
// TODO
unimplemented!()
@ -444,11 +681,27 @@ impl<'a> EnsureValidContext<'a> {
}
/// Prints to the hexchat buffer.
// TODO check if this triggers event hooks (which could call /close and invalidate the
// context).
// 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<F>(&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<F>(&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<F>(&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<F>(&mut self, timeout: i32, cb: F) -> TimerHookHandle where F: Fn(&mut PluginHandle) -> bool + 'static + ::std::panic::RefUnwindSafe {
self.ph.hook_timer(timeout, cb)
}
}
// ******* //
@ -463,6 +716,12 @@ impl<'a> EnsureValidContext<'a> {
// 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<Fn(&mut PluginHandle, Word, WordEol) -> Eat + ::std::panic::RefUnwindSafe>, *mut internals::Ph, PluginInfo);
/// Userdata type used by a server hook.
type ServerHookUd = (Box<Fn(&mut PluginHandle, Word, WordEol) -> Eat + ::std::panic::RefUnwindSafe>, *mut internals::Ph, PluginInfo);
/// Userdata type used by a print hook.
type PrintHookUd = (Box<Fn(&mut PluginHandle, Word) -> Eat + ::std::panic::RefUnwindSafe>, *mut internals::Ph, PluginInfo);
/// Userdata type used by a timer hook.
type TimerHookUd = (Rc<(Box<Fn(&mut PluginHandle) -> bool + ::std::panic::RefUnwindSafe>, *mut internals::Ph, PluginInfo)>, Rc<Cell<bool>>);
/// The contents of an empty CStr.
///
@ -480,6 +739,20 @@ 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<T>(ptr: *const T) -> Rc<T> {
// 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