hexavalent/
lib.rs

1//! Write HexChat plugins in Rust.
2//!
3//! To create your plugin:
4//! - Make a library crate with [`crate-type = "cdylib"`](https://doc.rust-lang.org/cargo/reference/manifest.html#building-dynamic-or-static-libraries).
5//! - Define a type, e.g. `struct MyPlugin`, to hold any state your plugin needs.
6//! - Implement the [`Plugin`] trait for `MyPlugin`.
7//! - Call [`export_plugin`] with the type `MyPlugin`, its name, description, and version.
8//!
9//! On Windows, it is recommended to add `-C target-feature=+crt-static` to your `RUSTFLAGS`,
10//! for example in [`<project root>/.cargo/config`](https://doc.rust-lang.org/cargo/reference/config.html).
11//! This ensures that your DLL does not dynamically import the MSVCRT.
12//!
13//! # Examples
14//!
15//! The following is a port of HexChat's [example](https://hexchat.readthedocs.io/en/latest/plugins.html#sample-plugin) "auto-op" plugin.
16//! It will automatically OP everyone who joins (so don't try this if you're in a real channel!),
17//! and can be toggled on and off with `/autooptoggle`.
18//!
19//! ```rust
20//! use std::cell::Cell;
21//! use hexavalent::{Plugin, PluginHandle, export_plugin};
22//! use hexavalent::event::print::Join;
23//! use hexavalent::hook::{Eat, Priority};
24//! use hexavalent::str::HexStr;
25//!
26//! struct AutoOpPlugin {
27//!     enabled: Cell<bool>,
28//! }
29//!
30//! impl Default for AutoOpPlugin {
31//!     fn default() -> Self {
32//!         Self {
33//!             enabled: Cell::new(true),
34//!         }
35//!     }
36//! }
37//!
38//! impl AutoOpPlugin {
39//!     fn autooptoggle_cb(&self, ph: PluginHandle<'_, Self>, _words: &[&HexStr]) -> Eat {
40//!         if !self.enabled.get() {
41//!             self.enabled.set(true);
42//!             ph.print("Auto-Oping now enabled!");
43//!         } else {
44//!             self.enabled.set(false);
45//!             ph.print("Auto-Oping now disabled!");
46//!         }
47//!         // eat this command so HexChat and other plugins can't process it
48//!         Eat::All
49//!     }
50//!
51//!     fn join_cb(&self, ph: PluginHandle<'_, Self>, args: [&HexStr; 4]) -> Eat {
52//!         let [nick, _channel, _host, _account] = args;
53//!         if self.enabled.get() {
54//!             // op ANYONE who joins
55//!             ph.command(format!("OP {}", nick));
56//!         }
57//!         // don't eat this event, HexChat needs to see it
58//!         Eat::None
59//!     }
60//! }
61//!
62//! impl Plugin for AutoOpPlugin {
63//!     fn init(&self, ph: PluginHandle<'_, Self>) {
64//!         ph.hook_command(
65//!             "AutoOpToggle",
66//!             "Usage: AUTOOPTOGGLE, turns OFF/ON Auto-Oping",
67//!             Priority::Normal,
68//!             Self::autooptoggle_cb,
69//!         );
70//!         ph.hook_print(Join, Priority::Normal, Self::join_cb);
71//!
72//!         ph.print("AutoOpPlugin loaded successfully!");
73//!     }
74//!
75//!     fn deinit(&self, ph: PluginHandle<'_, Self>) {
76//!         ph.print("Unloading AutoOpPlugin...");
77//!     }
78//! }
79//!
80//! export_plugin!(AutoOpPlugin, "AutoOp", "Auto-Ops anyone who joins", "0.1");
81//! ```
82//!
83//! # Safety
84//!
85//! In general, this library depends on HexChat invoking the plugin from only one thread.
86//! If that is not the case, this library provides no guarantees.
87//! (Although it is never explicitly stated that this is true, HexChat's plugin documentation says nothing of synchronization,
88//! and none of the example plugins have any. It also seems true in practice.)
89//!
90//! In debug mode (specifically, when `debug_assertions` is enabled), the current thread ID is checked every time the plugin is invoked,
91//! which can help detect misbehavior.
92
93#![allow(
94    clippy::get_first,
95    clippy::result_unit_err,
96    clippy::too_many_arguments,
97    clippy::type_complexity
98)]
99#![warn(
100    missing_debug_implementations,
101    missing_docs,
102    trivial_numeric_casts,
103    unreachable_pub,
104    unused_qualifications
105)]
106#![deny(unsafe_op_in_unsafe_fn)]
107
108#[macro_use]
109mod macros;
110
111mod ffi;
112mod iter;
113mod plugin;
114mod state;
115
116#[doc(hidden)]
117pub mod internal;
118
119pub mod context;
120pub mod event;
121pub mod gui;
122pub mod hook;
123pub mod info;
124pub mod list;
125pub mod mode;
126pub mod pref;
127pub mod str;
128pub mod strip;
129
130pub use plugin::{Plugin, PluginHandle};
131
132/// Defines the necessary exports for HexChat to load your plugin.
133///
134/// Do not define a `main` function; initialization should be performed in your plugin's [`Plugin::init`] function.
135///
136/// The type passed to `export_plugin` must implement [`Plugin`].
137///
138/// # Examples
139///
140/// ```rust
141/// use hexavalent::{Plugin, PluginHandle, export_plugin};
142///
143/// #[derive(Default)]
144/// struct NoopPlugin;
145///
146/// impl Plugin for NoopPlugin {
147///     fn init(&self, ph: PluginHandle<'_, Self>) {
148///         ph.print("Hello world!");
149///     }
150/// }
151///
152/// export_plugin!(NoopPlugin, "No-op", "Doesn't do anything", "1.0.0");
153/// ```
154///
155/// Cargo's environment variables can also be used to copy `name`, `description`, and `version` from `Cargo.toml`.
156///
157/// ```rust
158/// use hexavalent::{Plugin, PluginHandle, export_plugin};
159///
160/// #[derive(Default)]
161/// struct NoopPlugin;
162///
163/// impl Plugin for NoopPlugin {
164///     fn init(&self, ph: PluginHandle<'_, Self>) {
165///         ph.print("Hello world!");
166///     }
167/// }
168///
169/// export_plugin!(
170///     NoopPlugin,
171///     env!("CARGO_PKG_NAME"),
172///     env!("CARGO_PKG_DESCRIPTION"),
173///     env!("CARGO_PKG_VERSION"),
174/// );
175/// ```
176#[macro_export]
177macro_rules! export_plugin {
178    (
179        $plugin_ty:ty,
180        $name:expr,
181        $desc:expr,
182        $version:expr $(,)?
183    ) => {
184        #[no_mangle]
185        pub unsafe extern "C" fn hexchat_plugin_init(
186            plugin_handle: *mut $crate::internal::hexchat_plugin,
187            plugin_name: *mut *const ::std::os::raw::c_char,
188            plugin_desc: *mut *const ::std::os::raw::c_char,
189            plugin_version: *mut *const ::std::os::raw::c_char,
190            _arg: *mut ::std::os::raw::c_char,
191        ) -> ::std::os::raw::c_int {
192            const NAME: &::std::ffi::CStr =
193                match ::std::ffi::CStr::from_bytes_with_nul(concat!($name, "\0").as_bytes()) {
194                    Ok(x) => x,
195                    Err(_) => unreachable!(),
196                };
197            const DESC: &::std::ffi::CStr =
198                match ::std::ffi::CStr::from_bytes_with_nul(concat!($desc, "\0").as_bytes()) {
199                    Ok(x) => x,
200                    Err(_) => unreachable!(),
201                };
202            const VERSION: &::std::ffi::CStr =
203                match ::std::ffi::CStr::from_bytes_with_nul(concat!($version, "\0").as_bytes()) {
204                    Ok(x) => x,
205                    Err(_) => unreachable!(),
206                };
207
208            // Safety: these literals are null-terminated and 'static
209            *plugin_name = NAME.as_ptr();
210            *plugin_desc = DESC.as_ptr();
211            *plugin_version = VERSION.as_ptr();
212
213            $crate::internal::hexchat_plugin_init::<$plugin_ty>(plugin_handle)
214        }
215
216        #[no_mangle]
217        pub unsafe extern "C" fn hexchat_plugin_deinit(
218            plugin_handle: *mut $crate::internal::hexchat_plugin,
219        ) -> ::std::os::raw::c_int {
220            $crate::internal::hexchat_plugin_deinit::<$plugin_ty>(plugin_handle)
221        }
222    };
223}