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}