hexchat_api/thread_facilities.rs
1#![cfg(feature = "threadsafe")]
2
3//! This module provides facilities for accessing Hexchat from routines
4//! running on threads other than Hexchat's main thread.
5//!
6//! Hexchat's plugin API isn't inherently thread-safe, however plugins
7//! can spawn separate threads and invoke Hexchat's API by placing routines
8//! to execute on Hexchat's main thread.
9//!
10//! `main_thread()` makes it easy to declare a function, or closure, that
11//! contains Hexchat API calls. Once executed, it uses the timer feature
12//! of Hexchat to delegate. The function or closure can return any sendable
13//! cloneable value, and `main_thread()` will pass that back to the calling
14//! thread via an `AsyncResult` object. This can either be ignored, and
15//! the thread can continue doing other work, or `AsyncResult.get()` can be
16//! invoked on the result object; this call will block until the main thread
17//! has finished executing the callback.
18
19use std::collections::LinkedList;
20use std::sync::{Arc, Condvar, Mutex};
21use std::thread;
22
23use crate::hexchat::Hexchat;
24use crate::hexchat_entry_points::PHEXCHAT;
25use crate::{user_data::*, HexchatError};
26
27use UserData::*;
28
29const TASK_SPURT_SIZE: i32 = 5;
30const TASK_REST_MSECS: i64 = 2;
31
32// The type of the queue that closures will be added to and pulled from to run
33// on the main thread of Hexchat.
34type TaskQueue = LinkedList<Box<dyn Task>>;
35
36/// The task queue that other threads use to schedule tasks to run on the
37/// main thread. It is guarded by a `Mutex`.
38///
39static TASK_QUEUE: Mutex<Option<TaskQueue>> = Mutex::new(None);
40
41/// The main thread's ID is captured and used by `main_thread()` to determine
42/// whether it is being called from the main thread or not. If not, the
43/// callback can be invoked right away. Otherwise, it gets scheduled.
44///
45pub(crate) static mut MAIN_THREAD_ID: Option<thread::ThreadId> = None;
46
47/// Base trait for items placed on the task queue.
48///
49trait Task : Send {
50 fn execute(&mut self, hexchat: &Hexchat);
51 fn set_error(&mut self, error: &str);
52}
53
54/// A task that executes a closure on the main thread.
55///
56struct ConcreteTask<F, R>
57where
58 F: FnMut(&Hexchat) -> R,
59 R: Clone + Send,
60{
61 callback : F,
62 result : AsyncResult<R>,
63}
64
65impl<F, R> ConcreteTask<F, R>
66where
67 F: FnMut(&Hexchat) -> R,
68 R: Clone + Send,
69{
70 fn new(callback: F, result: AsyncResult<R>) -> Self {
71 ConcreteTask {
72 callback,
73 result,
74 }
75 }
76}
77
78impl<F, R> Task for ConcreteTask<F, R>
79where
80 F: FnMut(&Hexchat) -> R,
81 R: Clone + Send,
82{
83 /// Executes the closure and sets the result.
84 ///
85 fn execute(&mut self, hexchat: &Hexchat) {
86 self.result.set((self.callback)(hexchat));
87 }
88 /// When the task queue is being shut down, this will be called to set the
89 /// result to an error.
90 ///
91 fn set_error(&mut self, error: &str) {
92 self.result.set_error(error);
93 }
94}
95
96unsafe impl<F, R> Send for ConcreteTask<F, R>
97where
98 F: FnMut(&Hexchat) -> R,
99 R: Clone + Send,
100{}
101
102/// A result object that allows callbacks operating on a thread to send their
103/// return value to a receiver calling `get()` from another thread. Whether
104/// return data needs to be transferred or not, this object can be used to wait
105/// on the completion of a callback, thus providing synchronization between
106/// threads.
107///
108#[allow(clippy::type_complexity)]
109#[derive(Clone)]
110pub struct AsyncResult<T: Clone + Send> {
111 data: Arc<(Mutex<(Option<Result<T, HexchatError>>, bool)>, Condvar)>,
112}
113
114unsafe impl<T: Clone + Send> Send for AsyncResult<T> {}
115unsafe impl<T: Clone + Send> Sync for AsyncResult<T> {}
116
117impl<T: Clone + Send> AsyncResult<T> {
118 /// Constructor. Initializes the return data to None.
119 ///
120 pub (crate)
121 fn new() -> Self {
122 AsyncResult {
123 data: Arc::new((Mutex::new((None, false)), Condvar::new()))
124 }
125 }
126 /// Indicates whether the callback executing on another thread is done or
127 /// not. This can be used to poll for the result.
128 ///
129 pub fn is_done(&self) -> bool {
130 let (mtx, _) = &*self.data;
131 mtx.lock().unwrap().1
132 }
133 /// Blocking call to retrieve the return data from a callback on another
134 /// thread.
135 ///
136 pub fn get(&self) -> Result<T, HexchatError> {
137 let (mtx, cvar) = &*self.data;
138 let mut guard = mtx.lock().unwrap();
139 while !guard.1 {
140 guard = cvar.wait(guard).unwrap();
141 }
142 guard.0.take().unwrap()
143 }
144 /// Sets the return data for the async result. This will unblock the
145 /// receiver waiting on the result from `get()`.
146 ///
147 pub (crate)
148 fn set(&self, result: T) {
149 let (mtx, cvar) = &*self.data;
150 let mut guard = mtx.lock().unwrap();
151 *guard = (Some(Ok(result)), true);
152 cvar.notify_all();
153 }
154 fn set_error(&self, error: &str) {
155 use HexchatError::ThreadSafeOperationFailed as Error;
156 let (mtx, cvar) = &*self.data;
157 let mut guard = mtx.lock().unwrap();
158 *guard = (Some(Err(Error(error.into()))), true);
159 cvar.notify_all();
160 }
161}
162
163/// Executes a closure from the Hexchat main thread. This function returns
164/// immediately with an AsyncResult object that can be used to retrieve the
165/// result of the operation that will run on the main thread.
166///
167/// # Arguments
168/// * `callback` - The callback to execute on the main thread.
169///
170pub fn main_thread<F, R>(mut callback: F) -> AsyncResult<R>
171where
172 F: FnMut(&Hexchat) -> R + Sync + Send,
173 F: 'static + Send,
174 R: 'static + Clone + Send,
175{
176 if Some(thread::current().id()) == unsafe { MAIN_THREAD_ID } {
177 let result = callback(unsafe { &*PHEXCHAT });
178 let res = AsyncResult::new();
179 res.set(result);
180 res
181 } else {
182 let res = AsyncResult::new();
183 let cln = res.clone();
184 if let Some(queue) = TASK_QUEUE.lock().unwrap().as_mut() {
185 let task = Box::new(ConcreteTask::new(callback, cln));
186 queue.push_back(task);
187 }
188 else {
189 res.set_error("Task queue has been shut down.");
190 }
191 res
192 }
193}
194
195/// This initializes the fundamental thread-safe features of this library.
196/// A mutex guarded task queue is created, and a timer function is registered
197/// that handles the queue at intervals. If a thread requires fast response,
198/// the handler will field its requests one after another for up to
199/// `TASK_SPURT_SIZE` times without rest.
200///
201pub (crate)
202fn main_thread_init() {
203 unsafe { MAIN_THREAD_ID = Some(thread::current().id()) }
204
205 if TASK_QUEUE.lock().unwrap().is_none() {
206 *TASK_QUEUE.lock().unwrap() = Some(LinkedList::new());
207 let hex = unsafe { &*PHEXCHAT };
208
209 hex.hook_timer(
210 TASK_REST_MSECS,
211 move |_hc, _ud| {
212 if let Some(task_queue) = TASK_QUEUE.lock().unwrap().as_mut() {
213 let mut count = 1;
214
215 while let Some(mut task) = task_queue.pop_front() {
216 task.execute(hex);
217 count += 1;
218 if count > TASK_SPURT_SIZE {
219 break
220 }
221 }
222 1 // Keep going.
223 } else {
224 0 // Task queue is gone, remove timer callback.
225 }
226 },
227 NoData);
228 }
229}
230
231/// Called when the an addon is being unloaded. This eliminates the task queue.
232/// Any holders of `AsyncResult` objects that are blocked on `.get()` may be
233/// waiting forever. This can be called from addons if the thread-safe
234/// features aren't going to be utilized. No need to have a timer callback
235/// being invoked endlessly doing nothing.
236///
237pub (crate)
238fn main_thread_deinit() {
239 if let Some(mut queue) = TASK_QUEUE.lock().unwrap().take() {
240 while let Some(mut task) = queue.pop_front() {
241 task.set_error("Task queue is being shut down.");
242 }
243 }
244}
245
246/// Stops and removes the main thread task queue handler. Otherwise it will
247/// keep checking the queue while doing nothing useful - which isn't
248/// necessarily bad. Performance is unaffected either way.
249///
250/// Support for `main_thread()` is on by default. After this function is
251/// invoked, `main_thread()` should not be used and threads in general risk
252/// crashing the software if they try to access Hexchat directly without
253/// the `main_thread()`. `ThreadSafeContext` and `ThreadSafeListIterator`
254/// should also not be used after this function is called, since they rely on
255/// `main_thread()` internally.
256///
257/// # Safety
258/// While this will disable the handling of the main thread task queue, it
259/// doesn't prevent the plugin author from spawning threads and attempting to
260/// use the features of the threadsafe objects this crate provides. If the
261/// plugin author intends to use `ThreadSafeContext`, `ThreadSafeListIterator`,
262/// or invoke `main_thread()` directly, then this function should not be called.
263///
264#[deprecated(
265 since = "0.2.6",
266 note = "This function is no longer necessary. Threadsafe features can be \
267 turned off by specifying `features = []` in the Cargo.toml file \
268 for the `hexchat-api` dependency.")]
269pub unsafe fn turn_off_threadsafe_features() {
270 main_thread_deinit();
271}