hexchat_plugin/lib.rs
1// Hexchat Plugin API Bindings for Rust
2// Copyright (C) 2018, 2021 Soni L.
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as
6// published by the Free Software Foundation, either version 3 of the
7// License, or (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! Write hexchat plugins in Rust!
18//!
19//! This library provides safe API bindings for hexchat, but doesn't attempt to fix hexchat's own
20//! bugs. It makes no effort to stop you from unloading your own code while it's still running, for
21//! example.
22//!
23//! When using this library, it's strongly recommended to avoid heap-allocated statics (static
24//! mutexes, lazy_static, etc). This is because it's currently impossible to deallocate those on
25//! plugin unload. This can be worked around by placing those statics as fields in your plugin
26//! struct.
27//!
28//! This caveat does not apply to static assets (`static FOO: &'static str`, for example), but it
29//! does apply to thread-local storage.
30//!
31//! # Examples
32//!
33//! ```
34//! #[macro_use]
35//! extern crate hexchat_plugin;
36//!
37//! use std::sync::Mutex;
38//! use hexchat_plugin::{Plugin, PluginHandle, CommandHookHandle};
39//!
40//! #[derive(Default)]
41//! struct MyPlugin {
42//! cmd: Mutex<Option<CommandHookHandle>>
43//! }
44//!
45//! impl Plugin for MyPlugin {
46//! fn init(&self, ph: &mut PluginHandle, arg: Option<&str>) -> bool {
47//! ph.register("myplugin", "0.1.0", "my simple plugin");
48//! *self.cmd.lock().unwrap() = Some(ph.hook_command("hello-world", |ph, arg, arg_eol| {
49//! ph.print("Hello, World!");
50//! hexchat_plugin::EAT_ALL
51//! }, hexchat_plugin::PRI_NORM, Some("prints 'Hello, World!'")));
52//! true
53//! }
54//! }
55//!
56//! hexchat_plugin!(MyPlugin);
57//!
58//! # fn main() { } // satisfy the compiler, we can't actually run the code
59//! ```
60
61/*
62 * Some info about how HexChat does things:
63 *
64 * All strings passed across the C API are UTF-8.
65 * - Except `hexchat_get_info(ph, "libdirfs")`, so we need to be careful with that one.
66 *
67 * The pointers `name: *mut *const char, desc: *mut *const char, vers: *mut *const char` point to
68 * inside the ph - that is, they're aliased. Thus, we must never convert a ph to an & or &mut
69 * except as part of retrieving or setting values in it (e.g. `(*ph).hexchat_get_info` or
70 * `(*ph).userdata = value`).
71 *
72 * `hexchat_plugin_get_info` is never used, so we omit it. It would be impractical not to.
73 *
74 * These cause UB:
75 * `hexchat_command` may invalidate the plugin context.
76 * `hexchat_find_context` and `hexchat_nickcmp` use the plugin context without checking it.
77 * `hexchat_get_prefs` uses the plugin context if name == "state_cursor" or "id" (or anything with
78 * the same hash).
79 * `hexchat_list_get` uses the plugin context if name == "notify" (or anything with the same hash).
80 * `hexchat_list_str`, `hexchat_list_int`,
81 * `hexchat_emit_print`, `hexchat_emit_print_attrs` use the plugin context.
82 * `hexchat_send_modes` uses the plugin context.
83 * We need to wrap them (or, alternatively, hexchat_command). However, there's no (safe) way to get
84 * a valid context afterwards.
85 * - Actually that's a lie. Hexchat does a trick to give you a context as part of the channel list.
86 * We can use that to our advantage. I'm not sure if it's better to wrap hexchat_command or the
87 * other functions, tho.
88 * (Do we want to walk a linked list every time we use hexchat_command? I'd think
89 * hexchat_command is the most used API function... Plus, emit_print could indirectly
90 * invalidate the context as well.)
91 *
92 * `hexchat_send_modes` should only be used with channels; however, it doesn't cause UB - it just
93 * doesn't work.
94 *
95 * `hexchat_pluginpref_get_int`, `hexchat_pluginpref_get_str`, `hexchat_pluginpref_set_int`,
96 * `hexchat_pluginpref_set_str` cannot be used while `name`, `desc`, `vers` are null.
97 *
98 * `hexchat_plugin_init` receives an arg string. it may be null. this isn't documented anywhere.
99 */
100
101/*
102 * Some info about how we do things:
103 *
104 * DO NOT CALL printf/commandf/etc FAMILY OF FUNCTIONS. You can't avoid allocations, so just
105 * allocate some CStrings on the Rust side. It has the exact same effect, since those functions
106 * allocate internally anyway.
107 */
108
109/*
110 * Big list o' TODO:
111 * -[ ] Finish API support. [PRI-HIGH]
112 * -[x] word
113 * -[x] word_eol
114 * -[#] HEXCHAT_PRI_{HIGHEST, HIGH, NORM, LOW, LOWEST}
115 * -[x] HEXCHAT_EAT_{NONE, HEXCHAT, PLUGIN, ALL}
116 * -[ ] HEXCHAT_FD_{READ, WRITE, EXCEPTION, NOTSOCKET}
117 * -[x] hexchat_command (for commandf, use command(&format!("...")), it is equivalent.)
118 * -[x] hexchat_print (for printf, use print(&format!("...")), it is equivalent.)
119 * -[x] hexchat_emit_print
120 * -[x] hexchat_emit_print_attrs
121 * -[x] hexchat_send_modes
122 * -[x] hexchat_nickcmp
123 * -[ ] hexchat_strip
124 * -[x] ~~hexchat_free~~ not available - use Drop impls.
125 * -[x] ~~hexchat_event_attrs_create~~ not available - converted as needed
126 * -[x] ~~hexchat_event_attrs_free~~ not available - use Drop impls.
127 * -[x] hexchat_get_info
128 * -[ ] hexchat_get_prefs
129 * -[ ] hexchat_list_get, hexchat_list_fields, hexchat_list_next, hexchat_list_str,
130 * hexchat_list_int, hexchat_list_time, hexchat_list_free
131 * -[x] hexchat_hook_command
132 * -[ ] hexchat_hook_fd
133 * -[x] hexchat_hook_print
134 * -[x] hexchat_hook_print_attrs
135 * -[#] hexchat_hook_server (implemented through _attrs)
136 * -[x] hexchat_hook_server_attrs
137 * -[x] hexchat_hook_timer
138 * -[x] ~~hexchat_unhook~~ not available - use Drop impls
139 * -[x] hexchat_find_context
140 * -[x] hexchat_get_context
141 * -[x] hexchat_set_context
142 * -[ ] hexchat_pluginpref_set_str
143 * -[ ] hexchat_pluginpref_get_str
144 * -[ ] hexchat_pluginpref_set_int
145 * -[ ] hexchat_pluginpref_get_int
146 * -[ ] hexchat_pluginpref_delete
147 * -[ ] hexchat_pluginpref_list
148 * -[ ] hexchat_plugingui_add
149 * -[x] ~~hexchat_plugingui_remove~~ not available - use Drop impls.
150 * -[ ] Wrap contexts within something we keep track of. Mark them invalid when contexts are
151 * closed. [PRI-MAYBE]
152 * -[x] Anchor closures on the stack using Rc<T>. Many (most?) hooks are reentrant. As far as I
153 * know, all of them need this.
154 * -[x] Additionally, use a Cell<bool> for timers.
155 * -[ ] ???
156 */
157
158#[doc(hidden)]
159pub extern crate libc;
160
161mod internals;
162
163use std::borrow::Cow;
164use std::cell::Cell;
165use std::ffi::{CString, CStr};
166use std::marker::PhantomData;
167use std::mem;
168use std::ops;
169use std::panic::catch_unwind;
170use std::ptr;
171use std::rc::Rc;
172use std::rc::Weak as RcWeak;
173use std::str::FromStr;
174use std::thread;
175use std::time::{SystemTime, UNIX_EPOCH, Duration};
176
177// ****** //
178// PUBLIC //
179// ****** //
180
181// Consts
182
183// EAT_*
184/// Equivalent to HEXCHAT_EAT_NONE.
185pub const EAT_NONE: Eat = Eat { do_eat: 0 };
186/// Equivalent to HEXCHAT_EAT_HEXCHAT.
187pub const EAT_HEXCHAT: Eat = Eat { do_eat: 1 };
188/// Equivalent to HEXCHAT_EAT_PLUGIN.
189pub const EAT_PLUGIN: Eat = Eat { do_eat: 2 };
190/// Equivalent to HEXCHAT_EAT_ALL.
191pub const EAT_ALL: Eat = Eat { do_eat: 1 | 2 };
192
193// PRI_*
194/// Equivalent to HEXCHAT_PRI_HIGHEST
195pub const PRI_HIGHEST: i32 = 127;
196/// Equivalent to HEXCHAT_PRI_HIGH
197pub const PRI_HIGH: i32 = 64;
198/// Equivalent to HEXCHAT_PRI_NORM
199pub const PRI_NORM: i32 = 0;
200/// Equivalent to HEXCHAT_PRI_LOW
201pub const PRI_LOW: i32 = -64;
202/// Equivalent to HEXCHAT_PRI_LOWEST
203pub const PRI_LOWEST: i32 = -128;
204
205// Traits
206
207/// A hexchat plugin.
208pub trait Plugin {
209 /// Called to initialize the plugin.
210 fn init(&self, ph: &mut PluginHandle, arg: Option<&str>) -> bool;
211
212 /// Called to deinitialize the plugin.
213 ///
214 /// This is always called immediately prior to Drop::drop.
215 fn deinit(&self, _ph: &mut PluginHandle) {
216 }
217}
218
219// Structs
220
221/// A hexchat plugin handle, used to register hooks and interact with hexchat.
222///
223/// # Examples
224///
225/// ```
226/// use hexchat_plugin::{PluginHandle};
227///
228/// fn init(ph: &mut PluginHandle) {
229/// ph.register("myplug", "0.1.0", "my awesome plug");
230/// }
231/// ```
232pub struct PluginHandle {
233 ph: *mut internals::Ph,
234 skip_pri_ck: bool,
235 // Used for registration
236 info: PluginInfo,
237}
238
239/// Arguments passed to a hook, until the next argument.
240///
241/// # Examples
242///
243/// ```
244/// use hexchat_plugin::{PluginHandle, Word, WordEol, Eat};
245///
246/// fn cmd_foo(ph: &mut PluginHandle, arg: Word, arg_eol: WordEol) -> Eat {
247/// if arg.len() < 3 {
248/// ph.print("Not enough arguments");
249/// } else {
250/// ph.print(&format!("{} {} {}", arg[0], arg[1], arg[2]));
251/// }
252/// hexchat_plugin::EAT_ALL
253/// }
254///
255/// fn on_privmsg(ph: &mut PluginHandle, word: Word, word_eol: WordEol) -> Eat {
256/// if word.len() > 0 && word[0].starts_with('@') {
257/// ph.print("We have message tags!?");
258/// }
259/// hexchat_plugin::EAT_NONE
260/// }
261/// ```
262pub struct Word<'a> {
263 word: Vec<&'a str>
264}
265
266/// Arguments passed to a hook, until the end of the line.
267///
268/// # Examples
269///
270/// ```
271/// use hexchat_plugin::{PluginHandle, Word, WordEol, Eat};
272///
273/// fn cmd_foo(ph: &mut PluginHandle, arg: Word, arg_eol: WordEol) -> Eat {
274/// if arg.len() < 3 {
275/// ph.print("Not enough arguments");
276/// } else {
277/// ph.print(&format!("{} {} {}", arg[0], arg[1], arg_eol[2]));
278/// }
279/// hexchat_plugin::EAT_ALL
280/// }
281///
282/// fn on_privmsg(ph: &mut PluginHandle, word: Word, word_eol: WordEol) -> Eat {
283/// if word_eol.len() > 0 && word[0].starts_with('@') {
284/// ph.print("We have message tags!?");
285/// }
286/// hexchat_plugin::EAT_NONE
287/// }
288/// ```
289pub struct WordEol<'a> {
290 word_eol: Vec<&'a str>
291}
292
293/// A safety wrapper to ensure you're working with a valid context.
294///
295/// This mechanism attempts to reduce the likelihood of segfaults.
296pub struct EnsureValidContext<'a> {
297 ph: &'a mut PluginHandle,
298}
299
300/// Event attributes.
301#[derive(Clone)]
302pub struct EventAttrs<'a> {
303 /// Server time.
304 pub server_time: Option<SystemTime>,
305 _dummy: PhantomData<&'a ()>,
306}
307
308/// A status indicator for event callbacks. Indicates whether to continue processing, eat hexchat,
309/// eat plugin, or eat all.
310///
311/// Returned by most hooks.
312#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
313pub struct Eat {
314 do_eat: i32,
315}
316
317/// A command hook handle.
318#[must_use = "Hooks must be stored somewhere and are automatically unhooked on Drop"]
319pub struct CommandHookHandle {
320 ph: *mut internals::Ph,
321 hh: *const internals::HexchatHook,
322 // this does actually store an Rc<...>, but on the other side of the FFI.
323 _f: PhantomData<Rc<CommandHookUd>>,
324}
325
326/// A server hook handle.
327#[must_use = "Hooks must be stored somewhere and are automatically unhooked on Drop"]
328pub struct ServerHookHandle {
329 ph: *mut internals::Ph,
330 hh: *const internals::HexchatHook,
331 // this does actually store an Rc<...>, but on the other side of the FFI.
332 _f: PhantomData<Rc<ServerHookUd>>,
333}
334
335/// A print hook handle.
336#[must_use = "Hooks must be stored somewhere and are automatically unhooked on Drop"]
337pub struct PrintHookHandle {
338 ph: *mut internals::Ph,
339 hh: *const internals::HexchatHook,
340 // this does actually store an Rc<...>, but on the other side of the FFI.
341 _f: PhantomData<Rc<PrintHookUd>>,
342}
343
344/// A timer hook handle.
345#[must_use = "Hooks must be stored somewhere and are automatically unhooked on Drop"]
346pub struct TimerHookHandle {
347 ph: *mut internals::Ph,
348 hh: *const internals::HexchatHook,
349 // avoids issues
350 alive: Rc<Cell<bool>>,
351 // this does actually store an Rc<...>, but on the other side of the FFI.
352 _f: PhantomData<Rc<TimerHookUd>>,
353}
354
355/// A context.
356#[derive(Clone)]
357pub struct Context {
358 ctx: RcWeak<*const internals::HexchatContext>, // may be null
359 closure: Rc<Cell<Option<PrintHookHandle>>>,
360}
361
362// #[derive(Debug)] // doesn't work
363pub struct InvalidContextError<F: FnOnce(EnsureValidContext) -> R, R>(F);
364
365// Enums
366
367/// A hexchat_get_info key.
368#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Copy, Clone)]
369pub enum InfoId<'a> {
370 /// Returns the away message, or `None` if the user is not away.
371 Away,
372 /// Returns the current channel name.
373 Channel,
374 /// Returns the current charset.
375 Charset,
376 /// Returns the hexchat configuration directory, e.g. `/home/user/.config/hexchat`.
377 Configdir,
378 /// Returns the text event format string for the given text event name.
379 EventText(&'a str),
380 /// Returns the (real) hostname of the current server.
381 Host,
382 /// Returns the contents of the input box.
383 Inputbox,
384 /// Returns the library directory, e.g. `/usr/lib/hexchat`.
385 ///
386 /// May not always work, as this string isn't necessarily UTF-8, but local file system
387 /// encoding.
388 Libdirfs,
389 /// Returns the channel modes, if known, or `None`.
390 Modes,
391 /// Returns the current network name, or `None`.
392 Network,
393 /// Returns the user's current nick.
394 Nick,
395 /// Returns the user's nickserv password, if any, or `None`
396 Nickserv,
397 /// Returns the current server name, or `None` if you are not connected.
398 Server,
399 /// Returns the current channel topic.
400 Topic,
401 /// Returns the HexChat version string.
402 Version,
403 /// Returns the window status: "active", "hidden" or "normal".
404 WinStatus,
405}
406
407// ***** //
408// impls //
409// ***** //
410
411impl<'a> InfoId<'a> {
412 pub fn name(&self) -> Cow<'static, str> {
413 match *self {
414 InfoId::EventText(s) => {
415 let mut eventtext: String = "event_text ".into();
416 eventtext.push_str(&s);
417 eventtext.into()
418 },
419 InfoId::Away => "away".into(),
420 InfoId::Channel => "channel".into(),
421 InfoId::Charset => "charset".into(),
422 InfoId::Configdir => "configdir".into(),
423 InfoId::Host => "host".into(),
424 InfoId::Inputbox => "inputbox".into(),
425 InfoId::Libdirfs => "libdirfs".into(),
426 InfoId::Modes => "modes".into(),
427 InfoId::Network => "network".into(),
428 InfoId::Nick => "nick".into(),
429 InfoId::Nickserv => "nickserv".into(),
430 InfoId::Server => "server".into(),
431 InfoId::Topic => "topic".into(),
432 InfoId::Version => "version".into(),
433 InfoId::WinStatus => "win_status".into(),
434 }
435 }
436}
437
438impl<F: FnOnce(EnsureValidContext) -> R, R> InvalidContextError<F, R> {
439 pub fn get_closure(self) -> F {
440 self.0
441 }
442}
443
444impl Drop for CommandHookHandle {
445 fn drop(&mut self) {
446 unsafe {
447 let b = ((*self.ph).hexchat_unhook)(self.ph, self.hh) as *mut CommandHookUd;
448 // we assume b is not null. this should be safe.
449 // just drop it
450 mem::drop(Rc::from_raw(b));
451 }
452 }
453}
454impl Drop for ServerHookHandle {
455 fn drop(&mut self) {
456 unsafe {
457 let b = ((*self.ph).hexchat_unhook)(self.ph, self.hh) as *mut ServerHookUd;
458 // we assume b is not null. this should be safe.
459 // just drop it
460 mem::drop(Rc::from_raw(b));
461 }
462 }
463}
464impl Drop for PrintHookHandle {
465 fn drop(&mut self) {
466 unsafe {
467 let b = ((*self.ph).hexchat_unhook)(self.ph, self.hh) as *mut PrintHookUd;
468 // we assume b is not null. this should be safe.
469 // just drop it
470 mem::drop(Rc::from_raw(b));
471 }
472 }
473}
474impl Drop for TimerHookHandle {
475 fn drop(&mut self) {
476 if !self.alive.get() {
477 // already free'd.
478 return;
479 }
480 self.alive.set(false);
481 unsafe {
482 let b = ((*self.ph).hexchat_unhook)(self.ph, self.hh) as *mut TimerHookUd;
483 // we assume b is not null. this should be safe.
484 // just drop it
485 mem::drop(Rc::from_raw(b));
486 }
487 }
488}
489
490impl<'a> Word<'a> {
491 unsafe fn new(word: *const *const libc::c_char) -> Word<'a> {
492 let mut vec = vec![];
493 for i in 1..32 {
494 let w = *word.offset(i);
495 if !w.is_null() {
496 vec.push(CStr::from_ptr(w).to_str().expect("non-utf8 word - broken hexchat"))
497 }
498 }
499 while let Some(&"") = vec.last() {
500 vec.pop();
501 }
502 Word { word: vec }
503 }
504}
505
506impl<'a> ops::Deref for Word<'a> {
507 type Target = [&'a str];
508 fn deref(&self) -> &[&'a str] {
509 &self.word
510 }
511}
512
513impl<'a> WordEol<'a> {
514 unsafe fn new(word_eol: *const *const libc::c_char) -> WordEol<'a> {
515 let mut vec = vec![];
516 for i in 1..32 {
517 let w = *word_eol.offset(i);
518 if !w.is_null() {
519 vec.push(CStr::from_ptr(w).to_str().expect("non-utf8 word_eol - broken hexchat"))
520 }
521 }
522 while let Some(&"") = vec.last() {
523 vec.pop();
524 }
525 WordEol { word_eol: vec }
526 }
527}
528
529impl<'a> ops::Deref for WordEol<'a> {
530 type Target = [&'a str];
531 fn deref(&self) -> &[&'a str] {
532 &self.word_eol
533 }
534}
535
536impl PluginHandle {
537 fn new(ph: *mut internals::Ph, info: PluginInfo) -> PluginHandle {
538 PluginHandle {
539 ph, info, skip_pri_ck: false,
540 }
541 }
542
543 /// Registers this hexchat plugin. This must be called exactly once when the plugin is loaded.
544 ///
545 /// # Panics
546 ///
547 /// This function panics if this plugin is already registered.
548 ///
549 /// # Examples
550 ///
551 /// ```
552 /// use hexchat_plugin::PluginHandle;
553 ///
554 /// fn init(ph: &mut PluginHandle) {
555 /// ph.register("foo", "0.1.0", "my foo plugin");
556 /// }
557 /// ```
558 pub fn register(&mut self, name: &str, desc: &str, ver: &str) {
559 unsafe {
560 let info = self.info;
561 if !(*info.name).is_null() || !(*info.desc).is_null() || !(*info.vers).is_null() {
562 panic!("Attempt to re-register a plugin");
563 }
564 let name = CString::new(name).unwrap();
565 let desc = CString::new(desc).unwrap();
566 let ver = CString::new(ver).unwrap();
567 // these shouldn't panic. if they do, we'll need to free them afterwards.
568 (*info.name) = name.into_raw();
569 (*info.desc) = desc.into_raw();
570 (*info.vers) = ver.into_raw();
571 }
572 }
573
574 /// Returns this plugin's registered name.
575 ///
576 /// # Panics
577 ///
578 /// This function panics if this plugin is not registered.
579 pub fn get_name(&self) -> &str {
580 unsafe {
581 let info = self.info;
582 if !(*info.name).is_null() || !(*info.desc).is_null() || !(*info.vers).is_null() {
583 std::str::from_utf8_unchecked(CStr::from_ptr(*info.name).to_bytes())
584 } else {
585 panic!("Attempt to get the name of a plugin that was not yet registered.");
586 }
587 }
588 }
589
590 /// Returns this plugin's registered description.
591 ///
592 /// # Panics
593 ///
594 /// This function panics if this plugin is not registered.
595 pub fn get_description(&self) -> &str {
596 unsafe {
597 let info = self.info;
598 if !(*info.name).is_null() || !(*info.desc).is_null() || !(*info.vers).is_null() {
599 std::str::from_utf8_unchecked(CStr::from_ptr(*info.desc).to_bytes())
600 } else {
601 panic!("Attempt to get the description of a plugin that was not yet registered.");
602 }
603 }
604 }
605
606 /// Returns this plugin's registered version.
607 ///
608 /// # Panics
609 ///
610 /// This function panics if this plugin is not registered.
611 pub fn get_version(&self) -> &str {
612 unsafe {
613 let info = self.info;
614 if !(*info.name).is_null() || !(*info.desc).is_null() || !(*info.vers).is_null() {
615 std::str::from_utf8_unchecked(CStr::from_ptr(*info.vers).to_bytes())
616 } else {
617 panic!("Attempt to get the version of a plugin that was not yet registered.");
618 }
619 }
620 }
621
622 /// Ensures the current context is valid.
623 ///
624 /// # Panics
625 ///
626 /// This function may panic if it's called while hexchat is closing.
627 // NOTE: using a closure is nicer.
628 // TODO check if this is actually safe
629 pub fn ensure_valid_context<F, R>(&mut self, f: F) -> R where F: FnOnce(EnsureValidContext) -> R {
630 let ctx = self.get_context();
631 // need this here because we don't have NLL yet
632 let res = self.with_context(&ctx, f);
633 match res {
634 Result::Ok(r @ _) => r,
635 Result::Err(e @ _) => {
636 let nctx = self.find_valid_context().expect("ensure_valid_context failed (find_valid_context failed), was hexchat closing?");
637 self.with_context(&nctx, e.get_closure()).ok().expect("ensure_valid_context failed, was hexchat closing?")
638 }
639 }
640 }
641
642 /// Returns the current context.
643 ///
644 /// Note: The returned context may be invalid. Use [`set_context`] to check.
645 ///
646 /// [`set_context`]: #method.set_context
647 pub fn get_context(&mut self) -> Context {
648 let ctxp = unsafe { ((*self.ph).hexchat_get_context)(self.ph) };
649 // This needs to be fixed by hexchat. I cannot make the value become null when it's invalid
650 // without invoking UB. This is because I can't set_context to null.
651 let ok = unsafe { ((*self.ph).hexchat_set_context)(self.ph, ctxp) };
652 unsafe { wrap_context(self, if ok == 0 { ptr::null() } else { ctxp }) }
653 }
654
655 /// Sets the current context.
656 ///
657 /// Returns `true` if the context is valid, `false` otherwise.
658 pub fn set_context(&mut self, ctx: &Context) -> bool {
659 if let Some(ctx) = ctx.ctx.upgrade() {
660 unsafe {
661 ((*self.ph).hexchat_set_context)(self.ph, *ctx) != 0
662 }
663 } else {
664 false
665 }
666 }
667
668 /// Do something in a valid context.
669 ///
670 /// Note that this changes the active context and doesn't change it back.
671 ///
672 /// # Errors
673 ///
674 /// Returns `Err(InvalidContextError(f))` if the context is invalid. See [`set_context`]. Otherwise,
675 /// calls `f` and returns `Ok(its result)`.
676 ///
677 /// Note that `InvalidContextError` contains the original closure. This allows you to retry.
678 ///
679 /// [`set_context`]: #method.set_context
680 // this is probably safe to inline, and actually a good idea for ensure_valid_context
681 #[inline]
682 pub fn with_context<F, R>(&mut self, ctx: &Context, f: F) -> Result<R, InvalidContextError<F, R>> where F: FnOnce(EnsureValidContext) -> R {
683 if !self.set_context(ctx) {
684 Err(InvalidContextError(f))
685 } else {
686 Ok(f(EnsureValidContext { ph: self }))
687 }
688 }
689
690 /// Sets a command hook.
691 ///
692 /// # Examples
693 ///
694 /// ```
695 /// use hexchat_plugin::{PluginHandle, CommandHookHandle};
696 ///
697 /// fn register_commands(ph: &mut PluginHandle) -> Vec<CommandHookHandle> {
698 /// vec![
699 /// ph.hook_command("hello-world", |ph, arg, arg_eol| {
700 /// ph.print("Hello, World!");
701 /// hexchat_plugin::EAT_ALL
702 /// }, hexchat_plugin::PRI_NORM, Some("prints 'Hello, World!'")),
703 /// ]
704 /// }
705 /// ```
706 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 {
707 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 {
708 // hook may unhook itself.
709 // however, we don't wanna free it until it has returned.
710 let f: Rc<CommandHookUd> = rc_clone_from_raw(ud as *const CommandHookUd);
711 let ph = f.1;
712 match catch_unwind(move || {
713 let word = Word::new(word);
714 let word_eol = WordEol::new(word_eol);
715 (f.0)(&mut PluginHandle::new(f.1, f.2), word, word_eol).do_eat as libc::c_int
716 }) {
717 Result::Ok(v @ _) => v,
718 Result::Err(e @ _) => {
719 // if it's a &str or String, just print it
720 if let Some(estr) = e.downcast_ref::<&str>() {
721 hexchat_print_str(ph, estr, false);
722 } else if let Some(estring) = e.downcast_ref::<String>() {
723 hexchat_print_str(ph, &estring, false);
724 }
725 0 // EAT_NONE
726 }
727 }
728 }
729 let b: Rc<CommandHookUd> = Rc::new((Box::new(cb), self.ph, self.info));
730 let name = CString::new(cmd).unwrap();
731 let help_text = help.map(CString::new).map(Result::unwrap);
732 let bp = Rc::into_raw(b);
733 unsafe {
734 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 _);
735 assert!(!res.is_null());
736 CommandHookHandle { ph: self.ph, hh: res, _f: PhantomData }
737 }
738 }
739 /// Sets a server hook.
740 ///
741 /// # Examples
742 ///
743 /// ```
744 /// use hexchat_plugin::{PluginHandle, ServerHookHandle};
745 ///
746 /// fn register_server_hooks(ph: &mut PluginHandle) -> Vec<ServerHookHandle> {
747 /// vec![
748 /// ph.hook_server("PRIVMSG", |ph, word, word_eol| {
749 /// if word.len() > 0 && word[0].starts_with('@') {
750 /// ph.print("We have message tags!?");
751 /// }
752 /// hexchat_plugin::EAT_NONE
753 /// }, hexchat_plugin::PRI_NORM),
754 /// ]
755 /// }
756 /// ```
757 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 {
758 self.hook_server_attrs(cmd, move |ph, w, we, _| cb(ph, w, we), pri)
759 }
760 /// Sets a server hook, with attributes.
761 pub fn hook_server_attrs<F>(&mut self, cmd: &str, cb: F, pri: i32) -> ServerHookHandle where F: Fn(&mut PluginHandle, Word, WordEol, EventAttrs) -> Eat + 'static + ::std::panic::RefUnwindSafe {
762 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 {
763 // hook may unhook itself.
764 // however, we don't wanna free it until it has returned.
765 let f: Rc<ServerHookUd> = rc_clone_from_raw(ud as *const ServerHookUd);
766 let ph = f.1;
767 match catch_unwind(move || {
768 let word = Word::new(word);
769 let word_eol = WordEol::new(word_eol);
770 (f.0)(&mut PluginHandle::new(f.1, f.2), word, word_eol, (&*attrs).into()).do_eat as libc::c_int
771 }) {
772 Result::Ok(v @ _) => v,
773 Result::Err(e @ _) => {
774 // if it's a &str or String, just print it
775 if let Some(estr) = e.downcast_ref::<&str>() {
776 hexchat_print_str(ph, estr, false);
777 } else if let Some(estring) = e.downcast_ref::<String>() {
778 hexchat_print_str(ph, &estring, false);
779 }
780 0 // EAT_NONE
781 }
782 }
783 }
784 let b: Rc<ServerHookUd> = Rc::new((Box::new(cb), self.ph, self.info));
785 let name = CString::new(cmd).unwrap();
786 let bp = Rc::into_raw(b);
787 unsafe {
788 let res = ((*self.ph).hexchat_hook_server_attrs)(self.ph, name.as_ptr(), pri as libc::c_int, callback, bp as *mut _);
789 assert!(!res.is_null());
790 ServerHookHandle { ph: self.ph, hh: res, _f: PhantomData }
791 }
792 }
793 /// Sets a print hook.
794 ///
795 /// # Examples
796 ///
797 /// ```
798 /// use hexchat_plugin::{PluginHandle, PrintHookHandle};
799 ///
800 /// fn register_print_hooks(ph: &mut PluginHandle) -> Vec<PrintHookHandle> {
801 /// vec![
802 /// ph.hook_print("Channel Message", |ph, arg| {
803 /// if let Some(nick) = arg.get(0) {
804 /// if *nick == "KnOwN_SpAmMeR" {
805 /// return hexchat_plugin::EAT_ALL
806 /// }
807 /// }
808 /// hexchat_plugin::EAT_NONE
809 /// }, hexchat_plugin::PRI_NORM),
810 /// ]
811 /// }
812 /// ```
813 pub fn hook_print<F>(&mut self, name: &str, cb: F, mut pri: i32) -> PrintHookHandle where F: Fn(&mut PluginHandle, Word) -> Eat + 'static + ::std::panic::RefUnwindSafe {
814 // hmm, is there any way to avoid this code duplication?
815 // hook_print is special because dummy prints (keypresses, Close Context) are handled
816 // through here, but never through hook_print_attrs. :/
817 unsafe extern "C" fn callback(word: *const *const libc::c_char, ud: *mut libc::c_void) -> libc::c_int {
818 // hook may unhook itself.
819 // however, we don't wanna free it until it has returned.
820 let f: Rc<PrintHookUd> = rc_clone_from_raw(ud as *const PrintHookUd);
821 let ph = f.1;
822 match catch_unwind(move || {
823 let word = Word::new(word);
824 (f.0)(&mut PluginHandle::new(f.1, f.2), word, EventAttrs::new()).do_eat as libc::c_int
825 }) {
826 Result::Ok(v @ _) => v,
827 Result::Err(e @ _) => {
828 // if it's a &str or String, just print it
829 if let Some(estr) = e.downcast_ref::<&str>() {
830 hexchat_print_str(ph, estr, false);
831 } else if let Some(estring) = e.downcast_ref::<String>() {
832 hexchat_print_str(ph, &estring, false);
833 }
834 0 // EAT_NONE
835 }
836 }
837 }
838 if name == "Close Context" && (pri as libc::c_int == libc::c_int::min_value()) && !self.skip_pri_ck {
839 self.print("Warning: Attempted to hook Close Context with priority INT_MIN. Adjusting to INT_MIN+1.");
840 pri = (libc::c_int::min_value() + 1) as i32;
841 }
842 let b: Rc<PrintHookUd> = Rc::new((Box::new(move |ph, w, _| cb(ph, w)), self.ph, self.info));
843 let name = CString::new(name).unwrap();
844 let bp = Rc::into_raw(b);
845 unsafe {
846 let res = ((*self.ph).hexchat_hook_print)(self.ph, name.as_ptr(), pri as libc::c_int, callback, bp as *mut _);
847 assert!(!res.is_null());
848 PrintHookHandle { ph: self.ph, hh: res, _f: PhantomData }
849 }
850 }
851 /// Sets a print hook, with attributes.
852 pub fn hook_print_attrs<F>(&mut self, name: &str, cb: F, pri: i32) -> PrintHookHandle where F: Fn(&mut PluginHandle, Word, EventAttrs) -> Eat + 'static + ::std::panic::RefUnwindSafe {
853 unsafe extern "C" fn callback(word: *const *const libc::c_char, attrs: *const internals::HexchatEventAttrs, ud: *mut libc::c_void) -> libc::c_int {
854 // hook may unhook itself.
855 // however, we don't wanna free it until it has returned.
856 let f: Rc<PrintHookUd> = rc_clone_from_raw(ud as *const PrintHookUd);
857 let ph = f.1;
858 match catch_unwind(move || {
859 let word = Word::new(word);
860 (f.0)(&mut PluginHandle::new(f.1, f.2), word, (&*attrs).into()).do_eat as libc::c_int
861 }) {
862 Result::Ok(v @ _) => v,
863 Result::Err(e @ _) => {
864 // if it's a &str or String, just print it
865 if let Some(estr) = e.downcast_ref::<&str>() {
866 hexchat_print_str(ph, estr, false);
867 } else if let Some(estring) = e.downcast_ref::<String>() {
868 hexchat_print_str(ph, &estring, false);
869 }
870 0 // EAT_NONE
871 }
872 }
873 }
874 let b: Rc<PrintHookUd> = Rc::new((Box::new(cb), self.ph, self.info));
875 let name = CString::new(name).unwrap();
876 let bp = Rc::into_raw(b);
877 unsafe {
878 let res = ((*self.ph).hexchat_hook_print_attrs)(self.ph, name.as_ptr(), pri as libc::c_int, callback, bp as *mut _);
879 assert!(!res.is_null());
880 PrintHookHandle { ph: self.ph, hh: res, _f: PhantomData }
881 }
882 }
883 /// Sets a timer hook
884 ///
885 /// # Examples
886 ///
887 /// ```
888 /// use hexchat_plugin::{PluginHandle, TimerHookHandle};
889 ///
890 /// fn register_timers(ph: &mut PluginHandle) -> Vec<TimerHookHandle> {
891 /// vec![
892 /// ph.hook_timer(2000, |ph| {
893 /// ph.print("timer up!");
894 /// false
895 /// }),
896 /// ]
897 /// }
898 /// ```
899 pub fn hook_timer<F>(&mut self, timeout: i32, cb: F) -> TimerHookHandle where F: Fn(&mut PluginHandle) -> bool + 'static + ::std::panic::RefUnwindSafe {
900 unsafe extern "C" fn callback(ud: *mut libc::c_void) -> libc::c_int {
901 // hook may unhook itself.
902 // however, we don't wanna free it until it has returned.
903 let f: Rc<TimerHookUd> = rc_clone_from_raw(ud as *const TimerHookUd);
904 let alive = f.1.clone(); // clone the alive because why not
905 let f = f.0.clone();
906 let ph = f.1;
907 // we could technically free the Rc<TimerHookUd> here, I guess
908 match catch_unwind(move || {
909 (f.0)(&mut PluginHandle::new(f.1, f.2))
910 }) {
911 Result::Ok(true) => 1,
912 Result::Ok(false) => {
913 // avoid double-free
914 if !alive.get() {
915 return 0;
916 }
917 // mark it no longer alive
918 alive.set(false);
919 // HexChat will automatically free the hook.
920 // we just need to free the userdata.
921 mem::drop(Rc::from_raw(ud as *const TimerHookUd));
922 0
923 },
924 Result::Err(e @ _) => {
925 // if it's a &str or String, just print it
926 if let Some(estr) = e.downcast_ref::<&str>() {
927 hexchat_print_str(ph, estr, false);
928 } else if let Some(estring) = e.downcast_ref::<String>() {
929 hexchat_print_str(ph, &estring, false);
930 }
931 // avoid double-free
932 if !alive.get() {
933 return 0;
934 }
935 // mark it no longer alive
936 alive.set(false);
937 // HexChat will automatically free the hook.
938 // we just need to free the userdata.
939 mem::drop(Rc::from_raw(ud as *const TimerHookUd));
940 0
941 }
942 }
943 }
944 let alive = Rc::new(Cell::new(true));
945 let b: Rc<TimerHookUd> = Rc::new((Rc::new((Box::new(cb), self.ph, self.info)), alive.clone()));
946 let bp = Rc::into_raw(b);
947 unsafe {
948 let res = ((*self.ph).hexchat_hook_timer)(self.ph, timeout as libc::c_int, callback, bp as *mut _);
949 assert!(!res.is_null());
950 TimerHookHandle { ph: self.ph, hh: res, alive, _f: PhantomData }
951 }
952 }
953
954 /// Prints to the hexchat buffer.
955 // this checks the context internally. if it didn't, it wouldn't be safe to have here.
956 pub fn print<T: ToString>(&mut self, s: T) {
957 let s = s.to_string();
958 unsafe {
959 hexchat_print_str(self.ph, &*s, true);
960 }
961 }
962
963 /// Returns information on the current context.
964 ///
965 /// Note: `InfoId::Libdirfs` may return `None` or broken results if the result wouldn't be (valid) UTF-8.
966 // TODO this should be `id: InfoId`. fix in 0.3
967 pub fn get_info(&mut self, id: &InfoId) -> Option<String> {
968 let ph = self.ph;
969 let id_cstring = CString::new(&*id.name()).unwrap();
970 unsafe {
971 let res = ((*ph).hexchat_get_info)(ph, id_cstring.as_ptr());
972 if res.is_null() {
973 None
974 } else {
975 let s = CStr::from_ptr(res).to_owned().into_string();
976 if *id != InfoId::Libdirfs {
977 Some(s.expect("non-utf8 word - broken hexchat"))
978 } else {
979 s.ok()
980 }
981 }
982 }
983 }
984
985 // ******* //
986 // PRIVATE //
987 // ******* //
988
989 fn find_valid_context(&mut self) -> Option<Context> {
990 unsafe {
991 let ph = self.ph;
992 // TODO wrap this in a safer API, with proper Drop
993 #[allow(unused_mut)]
994 let mut list = ((*ph).hexchat_list_get)(ph, cstr(b"channels\0"));
995 // hexchat does this thing where it puts a context in a list_str.
996 // this *is* the proper way to do this
997 if ((*ph).hexchat_list_next)(ph, list) != 0 {
998 // if this panics we may leak some memory. it's not a big deal tho, as it indicates
999 // a bug in hexchat-plugin.rs.
1000 let ctx = ((*ph).hexchat_list_str)(ph, list, cstr(b"context\0")) as *const internals::HexchatContext;
1001 ((*ph).hexchat_list_free)(ph, list);
1002 Some(wrap_context(self, ctx))
1003 } else {
1004 ((*ph).hexchat_list_free)(ph, list);
1005 None
1006 }
1007 }
1008 }
1009}
1010
1011impl<'a> EventAttrs<'a> {
1012 pub fn new() -> EventAttrs<'a> {
1013 EventAttrs {
1014 server_time: None,
1015 _dummy: PhantomData,
1016 }
1017 }
1018}
1019
1020impl<'a> From<&'a internals::HexchatEventAttrs> for EventAttrs<'a> {
1021 fn from(other: &'a internals::HexchatEventAttrs) -> EventAttrs<'a> {
1022 EventAttrs {
1023 server_time: if other.server_time_utc > 0 { Some(UNIX_EPOCH + Duration::from_secs(other.server_time_utc as u64)) } else { None },
1024 _dummy: PhantomData,
1025 }
1026 }
1027}
1028
1029impl<'a> EnsureValidContext<'a> {
1030/*
1031 * These cause UB:
1032 * `hexchat_command` may invalidate the plugin context.
1033 * `hexchat_find_context` and `hexchat_nickcmp` use the plugin context without checking it.
1034 * `hexchat_get_prefs` uses the plugin context if name == "state_cursor" or "id" (or anything with
1035 * the same hash).
1036 * `hexchat_list_get` uses the plugin context if name == "notify" (or anything with the same hash).
1037 * `hexchat_list_str`, `hexchat_list_int`,
1038 * `hexchat_emit_print`, `hexchat_emit_print_attrs` use the plugin context.
1039 * `hexchat_send_modes` uses the plugin context.
1040 * We need to wrap them (or, alternatively, hexchat_command). However, there's no (safe) way to get
1041 * a valid context afterwards.
1042 * - Actually that's a lie. Hexchat does a trick to give you a context as part of the channel list.
1043 * We can use that to our advantage. I'm not sure if it's better to wrap hexchat_command or the
1044 * other functions, tho.
1045 * (Do we want to walk a linked list every time we use hexchat_command? I'd think
1046 * hexchat_command is the most used API function... Plus, emit_print could indirectly
1047 * invalidate the context as well.)
1048 *
1049 * For performance we put them behind an EnsureValidContext - things that don't invalidate the
1050 * context take an `&mut self`, things that do take an `self`.
1051 */
1052
1053 /// Finds an open context for the given servname and channel.
1054 pub fn find_context(&mut self, servname: Option<&str>, channel: Option<&str>) -> Option<Context> {
1055 // this was a mistake but oh well
1056 let ph = self.ph.ph;
1057 let servname = servname.map(|x| CString::new(x).unwrap());
1058 let channel = channel.map(|x| CString::new(x).unwrap());
1059 let ctx = unsafe {
1060 let sptr = servname.map(|x| x.as_ptr()).unwrap_or(ptr::null());
1061 let cptr = channel.map(|x| x.as_ptr()).unwrap_or(ptr::null());
1062 ((*ph).hexchat_find_context)(ph, sptr, cptr)
1063 };
1064 if ctx.is_null() {
1065 None
1066 } else {
1067 Some(unsafe { wrap_context(self.ph, ctx) })
1068 }
1069 }
1070
1071 /// Compares two nicks based on the server's case mapping.
1072 pub fn nickcmp(&mut self, nick1: &str, nick2: &str) -> ::std::cmp::Ordering {
1073 use std::cmp::Ordering;
1074 // this was a mistake but oh well
1075 let ph = self.ph.ph;
1076 // need to put this in a more permanent position than temporaries
1077 let nick1 = CString::new(nick1).unwrap();
1078 let nick2 = CString::new(nick2).unwrap();
1079 let res = unsafe {
1080 ((*ph).hexchat_nickcmp)(ph, nick1.as_ptr(), nick2.as_ptr())
1081 };
1082 if res < 0 {
1083 Ordering::Less
1084 } else if res > 0 {
1085 Ordering::Greater
1086 } else {
1087 Ordering::Equal
1088 }
1089 }
1090
1091 pub fn send_modes<'b, I: IntoIterator<Item=&'b str>>(&mut self, iter: I, mpl: i32, sign: char, mode: char) {
1092 // this was a mistake but oh well
1093 let ph = self.ph.ph;
1094 assert!(sign == '+' || sign == '-', "sign must be + or -");
1095 assert!(mode.is_ascii(), "mode must be ascii");
1096 assert!(mpl >= 0, "mpl must be non-negative");
1097 let v: Vec<CString> = iter.into_iter().map(|s| CString::new(s).unwrap()).collect();
1098 let mut v2: Vec<*const libc::c_char> = (&v).iter().map(|x| x.as_ptr()).collect();
1099 let arr: &mut [*const libc::c_char] = &mut *v2;
1100 unsafe {
1101 ((*ph).hexchat_send_modes)(ph, arr.as_mut_ptr(), arr.len() as libc::c_int,
1102 mpl as libc::c_int, sign as libc::c_char, mode as libc::c_char)
1103 }
1104 }
1105
1106 /// Executes a command.
1107 pub fn command(self, cmd: &str) {
1108 // this was a mistake but oh well
1109 let ph = self.ph.ph;
1110 // need to put this in a more permanent position than temporaries
1111 let cmd = CString::new(cmd).unwrap();
1112 unsafe {
1113 ((*ph).hexchat_command)(ph, cmd.as_ptr())
1114 }
1115 }
1116
1117 pub fn emit_print<'b, I: IntoIterator<Item=&'b str>>(self, event: &str, args: I) -> bool {
1118 let ph = self.ph.ph;
1119 let event = CString::new(event).unwrap();
1120 let mut args_cs: [Option<CString>; 4] = [None, None, None, None];
1121 {
1122 let mut iter = args.into_iter();
1123 for i in 0..4 {
1124 args_cs[i] = iter.next().map(|x| CString::new(x).unwrap());
1125 if args_cs[i].is_none() {
1126 break;
1127 }
1128 }
1129 if iter.next().is_some() {
1130 // it's better to panic so as to get bug reports when we need to increase this
1131 panic!("too many arguments to emit_print (max 4), or iterator not fused");
1132 }
1133 }
1134 let mut argv: [*const libc::c_char; 5] = [ptr::null(); 5];
1135 for i in 0..4 {
1136 argv[i] = args_cs[i].as_ref().map_or(ptr::null(), |s| s.as_ptr());
1137 }
1138 unsafe {
1139 ((*ph).hexchat_emit_print)(ph, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4]) != 0
1140 }
1141 }
1142
1143 pub fn emit_print_attrs<'b, I: IntoIterator<Item=&'b str>>(self, attrs: EventAttrs, event: &str, args: I) -> bool {
1144 let ph = self.ph.ph;
1145 let event = CString::new(event).unwrap();
1146 let mut args_cs: [Option<CString>; 4] = [None, None, None, None];
1147 {
1148 let mut iter = args.into_iter();
1149 for i in 0..4 {
1150 args_cs[i] = iter.next().map(|x| CString::new(x).unwrap());
1151 if args_cs[i].is_none() {
1152 break;
1153 }
1154 }
1155 if let Some(_) = iter.next() {
1156 // it's better to panic so as to get bug reports when we need to increase this
1157 panic!("too many arguments to emit_print_attrs (max 4), or iterator not fused");
1158 }
1159 }
1160 let mut argv: [*const libc::c_char; 5] = [ptr::null(); 5];
1161 for i in 0..4 {
1162 argv[i] = args_cs[i].as_ref().map_or(ptr::null(), |s| s.as_ptr());
1163 }
1164 let helper = HexchatEventAttrsHelper::new_with(ph, attrs);
1165 unsafe {
1166 ((*ph).hexchat_emit_print_attrs)(ph, helper.0, event.as_ptr(), argv[0], argv[1], argv[2], argv[3], argv[4]) != 0
1167 }
1168 }
1169
1170 // ******** //
1171 // FORWARDS //
1172 // ******** //
1173 // We can't just deref because then you could recursively ensure valid context and then it'd no
1174 // longer work.
1175
1176 pub fn get_context(&mut self) -> Context {
1177 self.ph.get_context()
1178 }
1179
1180 /// Sets the current context.
1181 ///
1182 /// Returns `true` if the context is valid, `false` otherwise.
1183 pub fn set_context(&mut self, ctx: &Context) -> bool {
1184 self.ph.set_context(ctx)
1185 }
1186
1187 /// Prints to the hexchat buffer.
1188 // as of hexchat 2.14.1, this does not call hooks.
1189 pub fn print<T: ToString>(&mut self, s: T) {
1190 self.ph.print(s)
1191 }
1192
1193 /// Sets a command hook
1194 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 {
1195 self.ph.hook_command(cmd, cb, pri, help)
1196 }
1197 /// Sets a server hook
1198 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 {
1199 self.ph.hook_server(cmd, cb, pri)
1200 }
1201 /// Sets a print hook
1202 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 {
1203 self.ph.hook_print(name, cb, pri)
1204 }
1205 /// Sets a timer hook
1206 pub fn hook_timer<F>(&mut self, timeout: i32, cb: F) -> TimerHookHandle where F: Fn(&mut PluginHandle) -> bool + 'static + ::std::panic::RefUnwindSafe {
1207 self.ph.hook_timer(timeout, cb)
1208 }
1209 pub fn get_info(&mut self, id: &InfoId) -> Option<String> {
1210 self.ph.get_info(id)
1211 }
1212}
1213
1214// ******* //
1215// PRIVATE //
1216// ******* //
1217
1218// Type aliases, used for safety type checking.
1219/// Userdata type used by a command hook.
1220// We actually do want RefUnwindSafe. This function may be called multiple times, and it's not
1221// automatically invalidated if it panics, so it may be called again after it panics. If you need
1222// mutable state, std provides std::sync::Mutex which has poisoning. Other interior mutability with
1223// poisoning could also be used. std doesn't have anything for single-threaded performance (yet),
1224// but hexchat isn't particularly performance-critical.
1225type CommandHookUd = (Box<dyn Fn(&mut PluginHandle, Word, WordEol) -> Eat + ::std::panic::RefUnwindSafe>, *mut internals::Ph, PluginInfo);
1226/// Userdata type used by a server hook.
1227type ServerHookUd = (Box<dyn Fn(&mut PluginHandle, Word, WordEol, EventAttrs) -> Eat + ::std::panic::RefUnwindSafe>, *mut internals::Ph, PluginInfo);
1228/// Userdata type used by a print hook.
1229type PrintHookUd = (Box<dyn Fn(&mut PluginHandle, Word, EventAttrs) -> Eat + ::std::panic::RefUnwindSafe>, *mut internals::Ph, PluginInfo);
1230/// Userdata type used by a timer hook.
1231type TimerHookUd = (Rc<(Box<dyn Fn(&mut PluginHandle) -> bool + ::std::panic::RefUnwindSafe>, *mut internals::Ph, PluginInfo)>, Rc<Cell<bool>>);
1232
1233/// The contents of an empty CStr.
1234///
1235/// This is useful where you need a non-null CStr.
1236// NOTE: MUST BE b"\0"!
1237const EMPTY_CSTRING_DATA: &[u8] = b"\0";
1238
1239/// Converts a nul-terminated const bstring to a C string.
1240///
1241/// # Panics
1242///
1243/// Panics if b contains embedded nuls.
1244// TODO const fn, once that's possible
1245fn cstr(b: &'static [u8]) -> *const libc::c_char {
1246 CStr::from_bytes_with_nul(b).unwrap().as_ptr()
1247}
1248
1249/// Clones an Rc straight from a raw pointer.
1250///
1251/// # Safety
1252///
1253/// This function is unsafe because `ptr` must hame come from `Rc::into_raw`.
1254unsafe fn rc_clone_from_raw<T>(ptr: *const T) -> Rc<T> {
1255 // this is a bit confusing to read, but basically, we get an Rc from the raw ptr, and increment
1256 // the refcount.
1257 // The construct mem::forget(rc.clone()) increments the refcount.
1258 let rc = Rc::from_raw(ptr);
1259 mem::forget(rc.clone());
1260 rc
1261}
1262
1263/// Converts a **valid** context pointer into a Context (Rust-managed) struct.
1264///
1265/// # Safety
1266///
1267/// This function doesn't validate the context.
1268unsafe fn wrap_context(ph: &mut PluginHandle, ctx: *const internals::HexchatContext) -> Context {
1269 let ctxp = std::panic::AssertUnwindSafe(Rc::new(ctx));
1270 let weak_ctxp = Rc::downgrade(&ctxp); // calling the Closure should drop the Context (sort of)
1271 let closure: Rc<Cell<Option<PrintHookHandle>>> = Rc::new(Cell::new(None));
1272 let hook = std::panic::AssertUnwindSafe(Rc::downgrade(&closure)); // dropping the Context should drop the Closure
1273 ph.skip_pri_ck = true;
1274 closure.set(Some(ph.hook_print("Close Context", move |ph, _| {
1275 // need to be careful not to recurse or leak memory
1276 let ph = ph.ph;
1277 let ctx = ((*ph).hexchat_get_context)(ph);
1278 if **ctxp == ctx {
1279 let _: Option<PrintHookHandle> = hook.upgrade().unwrap().replace(None);
1280 }
1281 EAT_NONE
1282 }, libc::c_int::min_value())));
1283 ph.skip_pri_ck = false;
1284 Context { ctx: weak_ctxp, closure }
1285}
1286
1287/// Prints an &str to hexchat, trying to avoid allocations.
1288///
1289/// # Safety
1290///
1291/// This function does not check the passed in argument.
1292///
1293/// # Panics
1294///
1295/// Panics if panic_on_nul is true and the string contains embedded nuls.
1296unsafe fn hexchat_print_str(ph: *mut internals::Ph, s: &str, panic_on_nul: bool) {
1297 match CString::new(s) {
1298 Result::Ok(cs @ _) => {
1299 let csr: &CStr = &cs;
1300 ((*ph).hexchat_print)(ph, csr.as_ptr())
1301 },
1302 e @ _ => if panic_on_nul {e.unwrap();}, // TODO nul_position?
1303 }
1304}
1305
1306/// Helper to manage owned internals::HexchatEventAttrs
1307struct HexchatEventAttrsHelper(*mut internals::HexchatEventAttrs, *mut internals::Ph);
1308
1309impl HexchatEventAttrsHelper {
1310 fn new(ph: *mut internals::Ph) -> Self {
1311 HexchatEventAttrsHelper(unsafe { ((*ph).hexchat_event_attrs_create)(ph) }, ph)
1312 }
1313
1314 fn new_with(ph: *mut internals::Ph, attrs: EventAttrs) -> Self {
1315 let helper = Self::new(ph);
1316 let v = attrs.server_time.or(Some(UNIX_EPOCH)).map(|st| match st.duration_since(UNIX_EPOCH) {
1317 Ok(n) => n.as_secs(),
1318 Err(_) => 0
1319 }).filter(|&st| st < (libc::time_t::max_value() as u64)).unwrap() as libc::time_t;
1320 unsafe { (*helper.0).server_time_utc = v; }
1321 helper
1322 }
1323}
1324
1325impl Drop for HexchatEventAttrsHelper {
1326 fn drop(&mut self) {
1327 unsafe {
1328 ((*self.1).hexchat_event_attrs_free)(self.1, self.0)
1329 }
1330 }
1331}
1332
1333/// Holds name, desc, vers
1334// This is kinda naughty - we modify these values after returning from hexchat_plugin_init, during
1335// the deinitialization.
1336// However, if my reading of the HexChat code is correct, this is "ok".
1337#[derive(Copy, Clone)]
1338struct PluginInfo {
1339 name: *mut *const libc::c_char,
1340 desc: *mut *const libc::c_char,
1341 vers: *mut *const libc::c_char,
1342}
1343
1344impl PluginInfo {
1345 /// Creates a PluginInfo.
1346 ///
1347 /// # Panics
1348 ///
1349 /// This function explicitly doesn't panic. Call unwrap() on the result instead.
1350 fn new(name: *mut *const libc::c_char, desc: *mut *const libc::c_char, vers: *mut *const libc::c_char) -> Option<PluginInfo> {
1351 if name.is_null() || desc.is_null() || vers.is_null() || name == desc || desc == vers || name == vers {
1352 None
1353 } else {
1354 Some(unsafe { PluginInfo::new_unchecked(name, desc, vers) })
1355 }
1356 }
1357
1358 /// Creates a PluginInfo without checking the arguments.
1359 ///
1360 /// # Safety
1361 ///
1362 /// This function is unsafe, as it doesn't check the validity of the arguments. You're expected
1363 /// to only pass in non-aliased non-null pointers. Use new if unsure.
1364 unsafe fn new_unchecked(name: *mut *const libc::c_char, desc: *mut *const libc::c_char, vers: *mut *const libc::c_char) -> PluginInfo {
1365 PluginInfo {
1366 name, desc, vers
1367 }
1368 }
1369
1370 /// Drop relevant CStrings.
1371 ///
1372 /// # Safety
1373 ///
1374 /// This function is unsafe because calling it may trigger Undefined Behaviour. See also
1375 /// [CString::from_raw].
1376 ///
1377 /// [from_raw]: https://doc.rust-lang.org/std/ffi/struct.CString.html#method.from_raw
1378 unsafe fn drop_info(&mut self) {
1379 if !(*self.name).is_null() {
1380 mem::drop(CString::from_raw(*self.name as *mut _));
1381 *self.name = cstr(EMPTY_CSTRING_DATA);
1382 }
1383 if !(*self.desc).is_null() {
1384 mem::drop(CString::from_raw(*self.desc as *mut _));
1385 *self.desc = cstr(EMPTY_CSTRING_DATA);
1386 }
1387 if !(*self.vers).is_null() {
1388 mem::drop(CString::from_raw(*self.vers as *mut _));
1389 *self.vers = cstr(EMPTY_CSTRING_DATA);
1390 }
1391 }
1392}
1393
1394/// Plugin data stored in the hexchat plugin_handle.
1395struct PhUserdata {
1396 plug: Box<dyn Plugin>,
1397 pluginfo: PluginInfo,
1398}
1399
1400/// Puts the userdata in the plugin handle.
1401///
1402/// # Safety
1403///
1404/// This function is unsafe because it doesn't check if the pointer is valid.
1405///
1406/// Improper use of this function can leak memory.
1407unsafe fn put_userdata(ph: *mut internals::Ph, ud: Box<PhUserdata>) {
1408 (*ph).userdata = Box::into_raw(ud) as *mut libc::c_void;
1409}
1410
1411// unsafe fn get_userdata(ph: *mut internals::Ph) -> *const PhUserdata {
1412// (*ph).userdata as *const _
1413// }
1414
1415/// Pops the userdata from the plugin handle.
1416///
1417/// # Safety
1418///
1419/// This function is unsafe because it doesn't check if the pointer is valid.
1420unsafe fn pop_userdata(ph: *mut internals::Ph) -> Box<PhUserdata> {
1421 Box::from_raw(mem::replace(&mut (*ph).userdata, ptr::null_mut()) as *mut PhUserdata)
1422}
1423
1424// *********************** //
1425// PUBLIC OUT OF NECESSITY //
1426// *********************** //
1427
1428#[doc(hidden)]
1429pub unsafe fn hexchat_plugin_init<T>(plugin_handle: *mut libc::c_void,
1430 plugin_name: *mut *const libc::c_char,
1431 plugin_desc: *mut *const libc::c_char,
1432 plugin_version: *mut *const libc::c_char,
1433 arg: *const libc::c_char) -> libc::c_int
1434 where T: Plugin + Default + 'static {
1435 if plugin_handle.is_null() || plugin_name.is_null() || plugin_desc.is_null() || plugin_version.is_null() {
1436 // we can't really do anything here.
1437 eprintln!("hexchat_plugin_init called with a null pointer that shouldn't be null - broken hexchat");
1438 // TODO maybe call abort.
1439 return 0;
1440 }
1441 let ph = plugin_handle as *mut internals::Ph;
1442 // clear the "userdata" field first thing - if the deinit function gets called (wrong hexchat
1443 // version, other issues), we don't wanna try to drop the hexchat_dummy or hexchat_read_fd
1444 // function as if it were a Box!
1445 (*ph).userdata = ptr::null_mut();
1446 // read the filename so we can pass it on later.
1447 let filename = if !(*plugin_name).is_null() {
1448 if let Ok(fname) = CStr::from_ptr(*plugin_name).to_owned().into_string() {
1449 fname
1450 } else {
1451 eprintln!("failed to convert filename to utf8 - broken hexchat");
1452 return 0;
1453 }
1454 } else {
1455 // no filename specified for some reason, but we can still load
1456 String::new() // empty string
1457 };
1458 // these may be null, unless initialization is successful.
1459 // we set them to null as markers.
1460 *plugin_name = ptr::null();
1461 *plugin_desc = ptr::null();
1462 *plugin_version = ptr::null();
1463 // do some version checks for safety
1464 // NOTE: calling hexchat functions with null plugin_name, plugin_desc, plugin_version is a bit
1465 // dangerous. this particular case is "ok".
1466 {
1467 let ver = ((*ph).hexchat_get_info)(ph, cstr(b"version\0")); // this shouldn't panic
1468 let cstr = CStr::from_ptr(ver);
1469 if let Ok(ver) = cstr.to_str() {
1470 let mut iter = ver.split('.');
1471 let a = iter.next().map(i32::from_str).and_then(Result::ok).unwrap_or(0);
1472 let b = iter.next().map(i32::from_str).and_then(Result::ok).unwrap_or(0);
1473 let c = iter.next().map(i32::from_str).and_then(Result::ok).unwrap_or(0);
1474 // 2.9.6 or greater
1475 if !(a > 2 || (a == 2 && (b > 9 || (b == 9 && (c > 6 || (c == 6)))))) {
1476 return 0;
1477 }
1478 } else {
1479 return 0;
1480 }
1481 }
1482 let mut pluginfo = if let Some(pluginfo) = PluginInfo::new(plugin_name, plugin_desc, plugin_version) {
1483 pluginfo
1484 } else {
1485 return 0;
1486 };
1487 let r: thread::Result<Option<Box<_>>> = {
1488 catch_unwind(move || {
1489 let mut pluginhandle = PluginHandle::new(ph, pluginfo);
1490 let plug = T::default();
1491 if plug.init(&mut pluginhandle, if !arg.is_null() { Some(CStr::from_ptr(arg).to_str().expect("arg not valid utf-8 - broken hexchat")) } else { None }) {
1492 if !(pluginfo.name.is_null() || pluginfo.desc.is_null() || pluginfo.vers.is_null()) {
1493 Some(Box::new(PhUserdata { plug: Box::new(plug), pluginfo }))
1494 } else {
1495 // TODO log: forgot to call register
1496 None
1497 }
1498 } else {
1499 None
1500 }
1501 })
1502 };
1503 match r {
1504 Result::Ok(Option::Some(plug @ _)) => {
1505 if (*plugin_name).is_null() || (*plugin_desc).is_null() || (*plugin_version).is_null() {
1506 // TODO deallocate any which are non-null
1507 pluginfo.drop_info();
1508 0
1509 } else {
1510 put_userdata(ph, plug);
1511 1
1512 }
1513 },
1514 r @ _ => {
1515 // if the initialization fails, deinit doesn't get called, so we need to clean up
1516 // ourselves.
1517
1518 // TODO might leak pluginfo on panic?
1519
1520 if let Err(_) = r {
1521 // TODO try to log panic?
1522 }
1523 0
1524 },
1525 }
1526}
1527
1528#[doc(hidden)]
1529pub unsafe fn hexchat_plugin_deinit<T>(plugin_handle: *mut libc::c_void) -> libc::c_int where T: Plugin {
1530 let mut safe_to_unload = 1;
1531 // plugin_handle should never be null, but just in case.
1532 if !plugin_handle.is_null() {
1533 let ph = plugin_handle as *mut internals::Ph;
1534 // userdata should also never be null.
1535 if !(*ph).userdata.is_null() {
1536 {
1537 let mut info: Option<PluginInfo> = None;
1538 {
1539 let mut ausinfo = ::std::panic::AssertUnwindSafe(&mut info);
1540 safe_to_unload = if catch_unwind(move || {
1541 let userdata = *pop_userdata(ph);
1542 **ausinfo = Some(userdata.pluginfo);
1543 userdata.plug.deinit(&mut PluginHandle::new(ph, userdata.pluginfo));
1544 }).is_ok() { 1 } else { 0 };
1545 }
1546 if let Some(mut info) = info {
1547 info.drop_info();
1548 } else {
1549 eprintln!("I have no idea tbh, I didn't know `pop_userdata` could panic!");
1550 }
1551 }
1552 } else {
1553 eprintln!("null userdata in hexchat_plugin_deinit - broken hexchat or broken hexchat-plugin.rs");
1554 }
1555 } else {
1556 eprintln!("hexchat_plugin_deinit called with a null plugin_handle - broken hexchat");
1557 }
1558 safe_to_unload
1559}
1560
1561/// Exports a hexchat plugin.
1562#[macro_export]
1563macro_rules! hexchat_plugin {
1564 ($t:ty) => {
1565 #[no_mangle]
1566 pub unsafe extern "C" fn hexchat_plugin_init(plugin_handle: *mut $crate::libc::c_void,
1567 plugin_name: *mut *const $crate::libc::c_char,
1568 plugin_desc: *mut *const $crate::libc::c_char,
1569 plugin_version: *mut *const $crate::libc::c_char,
1570 arg: *const $crate::libc::c_char) -> $crate::libc::c_int {
1571 $crate::hexchat_plugin_init::<$t>(plugin_handle, plugin_name, plugin_desc, plugin_version, arg)
1572 }
1573 #[no_mangle]
1574 pub unsafe extern "C" fn hexchat_plugin_deinit(plugin_handle: *mut $crate::libc::c_void) -> $crate::libc::c_int {
1575 $crate::hexchat_plugin_deinit::<$t>(plugin_handle)
1576 }
1577 // unlike what the documentation states, there's no need to define hexchat_plugin_get_info.
1578 // so we don't. it'd be impossible to make it work well with rust anyway.
1579 };
1580}