cxx_qt/
threading.rs

1// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
2// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
3//
4// SPDX-License-Identifier: MIT OR Apache-2.0
5
6use core::{marker::PhantomData, mem::MaybeUninit, pin::Pin};
7use cxx::ExternType;
8use thiserror::Error;
9
10use crate::Threading;
11
12/// Errors that can occur from CXX-Qt
13#[derive(Error, Debug)]
14#[non_exhaustive]
15pub enum ThreadingQueueError {
16    /// Threading failed as the object has been destroyed
17    #[error("Cannot queue function pointer as object has been destroyed")]
18    ObjectDestroyed,
19    /// Threading failed calling invokeMethod on the object
20    #[error("Cannot queue function pointer as invokeMethod on object failed")]
21    InvokeMethodFailed,
22    /// Threading failed with unknown error
23    #[error("Cannot queue as an unknown error occurred")]
24    Unknown,
25}
26
27impl From<u8> for ThreadingQueueError {
28    fn from(value: u8) -> Self {
29        match value {
30            1 => Self::ObjectDestroyed,
31            2 => Self::InvokeMethodFailed,
32            _others => Self::Unknown,
33        }
34    }
35}
36
37/// A threading helper which is created from a QObject that implements [Threading].
38///
39/// This allows for queueing closures onto the Qt event loop from a background thread
40/// as [CxxQtThread] implements [Send].
41///
42/// When the Rust thread needs to update a value in the QObject it can then queue a closure to the thread.
43/// This closure will be executed on the thread the QObject lives in while holding a lock on the Rust object.
44/// Updating the QObject is then thread-safe.
45///
46/// See the [Threading] example for more information.
47#[repr(C)]
48pub struct CxxQtThread<T>
49where
50    T: Threading,
51{
52    // The layout is one std::shared_ptr, which is two pointers in size
53    _space: MaybeUninit<[usize; 2]>,
54    _value: PhantomData<T>,
55}
56
57// Safety:
58//
59// Static checks on the C++ side to ensure the size is the same.
60unsafe impl<T> ExternType for CxxQtThread<T>
61where
62    T: ExternType + Threading,
63{
64    type Id = T::ThreadingTypeId;
65    type Kind = cxx::kind::Trivial;
66}
67
68impl<T> Clone for CxxQtThread<T>
69where
70    T: Threading,
71{
72    fn clone(&self) -> Self {
73        T::threading_clone(self)
74    }
75}
76
77impl<T> Drop for CxxQtThread<T>
78where
79    T: Threading,
80{
81    fn drop(&mut self) {
82        T::threading_drop(self);
83    }
84}
85
86// CxxQtThread is safe to be sent across threads and handles
87// locking and checks with the original QObject to prevent issues
88unsafe impl<T> Send for CxxQtThread<T> where T: Threading {}
89
90// CxxQtThread is safe to use as a reference in parallel from multiple
91// places as it protects the queue call and the closure with mutexes
92unsafe impl<T> Sync for CxxQtThread<T> where T: Threading {}
93
94impl<T> CxxQtThread<T>
95where
96    T: Threading,
97{
98    /// Queue the given closure onto the Qt event loop for this QObject
99    ///
100    /// The first argument of the closure is a pinned mutable reference to the QObject.
101    /// With this parameter, you can then update the QObject to reflect any state changes that have occured in the background thread.
102    pub fn queue<F>(&self, f: F) -> Result<(), crate::ThreadingQueueError>
103    where
104        F: FnOnce(Pin<&mut T>),
105        F: Send + 'static,
106    {
107        T::queue(self, f)
108    }
109
110    /// Checks whether the associated `QObject` has been destroyed.
111    ///
112    /// This method only confirms if the `QObject` has already been destroyed.
113    /// It does not guarantee that the `QObject` remains alive for any
114    /// subsequent operations. There is a potential race condition when using
115    /// `is_destroyed()` before calling `queue`. Specifically, the `QObject` may
116    /// be destroyed after the check but before the `queue` call.
117    ///
118    /// For example:
119    /// ```rust,ignore
120    /// if !thread.is_destroyed() {
121    ///     thread.queue(/*...*/).unwrap();
122    /// }
123    /// ```
124    /// In this scenario, the `QObject` might be destroyed between the
125    /// `is_destroyed` check and the `queue` invocation, resulting in a panic.
126    ///
127    /// To handle such cases safely, it is recommended to call `queue(...).ok()`
128    /// directly without checking `is_destroyed()`. This approach allows you to
129    /// handle the potential failure gracefully without risking a panic.
130    ///
131    /// However, `is_destroyed()` can still be useful in scenarios where you
132    /// need to control loops or perform cleanup operations based on the
133    /// destruction status of the `QObject`. For instance:
134    /// ```rust,ignore
135    /// while !thread.is_destroyed() {
136    ///     thread.queue(/*...*/).ok();
137    /// }
138    /// ```
139    pub fn is_destroyed(&self) -> bool {
140        T::is_destroyed(self)
141    }
142}