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}