qtbridge_runtime/qml_method_invoker.rs
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only
3
4use std::sync::Arc;
5use std::sync::atomic::{AtomicBool, Ordering};
6use qtbridge_type_lib::{QObject, QVariantList};
7
8use crate::QObjectHolder;
9
10#[cxx::bridge]
11pub mod ffi {
12 #[allow(clippy::missing_safety_doc)]
13 unsafe extern "C++" {
14 include!("qtbridge-type-lib/src/core/qobject/cpp/qobject.h");
15 type QObject = qtbridge_type_lib::QObject;
16 include!("qtbridge-type-lib/src/core/qlist/cpp/qlist_qvariant.h");
17 type QList_QVariant = qtbridge_type_lib::QList_QVariant;
18
19 include!("cpp/qml_method_invoker.h");
20
21 unsafe fn connect_destroyed_callback(obj: *mut QObject, flag_ptr: usize);
22
23 unsafe fn invoke_method(obj: *mut QObject, name: &str, args: &QList_QVariant) -> bool;
24 }
25
26 extern "Rust" {
27 fn on_qobject_destroyed(flag_ptr: usize);
28 }
29}
30
31fn on_qobject_destroyed(flag_ptr: usize) {
32 let arc = unsafe { Arc::from_raw(flag_ptr as *const AtomicBool) };
33 arc.store(false, Ordering::Release);
34}
35
36/// Invokes a slot or signal by name on a [`QmlMethodInvoker`].
37///
38/// Without extra arguments, delegates to [`QmlMethodInvoker::invoke_method`].
39/// With extra arguments, constructs the argument list and delegates to
40/// [`QmlMethodInvoker::invoke_method_with_args`].
41///
42/// ```
43/// use qtbridge::invoke_method;
44/// use qtbridge::{qobject, QmlObject};
45///
46/// #[derive(Default)]
47/// pub struct MyClass { }
48///
49/// #[qobject]
50/// impl MyClass {
51/// #[qslot]
52/// fn set_value(&mut self, value: i32) { }
53///
54/// #[qslot]
55/// fn reset(&self) { }
56/// }
57///
58/// let obj = MyClass::default_with_attached_qobject();
59/// let invoker = obj.borrow().get_qml_method_invoker();
60/// invoke_method!(invoker, "reset");
61/// invoke_method!(invoker, "setValue", 42);
62/// ```
63///
64#[macro_export]
65macro_rules! invoke_method {
66 ($invoker:expr, $name:expr $(,)?) => {{
67 $invoker.invoke_method($name)
68 }};
69
70 ($invoker:expr, $name:expr, $($arg:expr),+ $(,)?) => {{
71 let args: qtbridge::qtbridge_type_lib::QVariantList = [
72 $(qtbridge::qtbridge_runtime::QVariantConvertible::to_qvariant(&$arg)),+
73 ]
74 .iter()
75 .collect();
76 $invoker.invoke_method_with_args($name, &args)
77 }};
78}
79
80/// A thread-safe handle for invoking slots and signals on a `#[qobject]` instance.
81///
82/// Calls are scheduled on the Qt event loop and execute on the Qt thread.
83/// If the target object has been dropped, calls are silently discarded.
84///
85/// Arguments follow the same coercion rules QML applies when it calls a
86/// method: an argument matches a parameter as long as it can be converted to
87/// the parameter's type. These rules apply uniformly, even when a Rust method
88/// is invoked from Rust; this path does *not* enforce Rust's exact-type
89/// semantics, so conversions QML permits (for example between numeric types,
90/// or between a number and its textual form) are permitted here too.
91///
92/// When a call executes, Qt borrows the `Rc<RefCell<_>>` held by the QML
93/// engine. If the object is already mutably borrowed on the Qt thread at
94/// that moment, the call will panic.
95///
96/// Obtain an instance via [`QmlObject::get_qml_method_invoker`](crate::QmlObject::get_qml_method_invoker).
97///
98/// # Example
99///
100/// ```
101/// # use qtbridge::{qobject, QmlObject};
102/// # #[qobject]
103/// # pub mod example {
104/// # #[derive(Default)]
105/// # pub struct Backend {}
106/// # impl Backend {
107/// # #[qsignal]
108/// # pub fn data_ready(&mut self);
109/// # }
110/// # }
111/// # use example::Backend;
112/// let backend = Backend::default_with_attached_qobject();
113/// let invoker = backend.borrow().get_qml_method_invoker();
114/// std::thread::spawn(move || {
115/// invoker.invoke_method("dataReady");
116/// }).join().unwrap();
117/// ```
118pub struct QmlMethodInvoker {
119 obj: *mut QObject,
120 alive: Arc<AtomicBool>,
121}
122
123unsafe impl Send for QmlMethodInvoker {}
124
125impl QmlMethodInvoker {
126
127 /// Creates a `QmlMethodInvoker` for `target` and tracks its lifetime.
128 ///
129 /// Prefer [`QmlObject::get_qml_method_invoker`](crate::QmlObject::get_qml_method_invoker) over calling this directly.
130 pub fn new<T: QObjectHolder>(target: &T) -> Self {
131 let obj = target.get_qobject_ptr();
132 let alive = Arc::new(AtomicBool::new(true));
133 let flag_ptr = Arc::into_raw(alive.clone()) as usize;
134 unsafe { ffi::connect_destroyed_callback(obj, flag_ptr) };
135 Self { obj, alive }
136 }
137
138 fn is_alive(&self) -> bool {
139 self.alive.load(Ordering::Acquire)
140 }
141
142 /// Schedules `name` to run on the Qt thread via the Qt event loop.
143 ///
144 /// `name` is resolved over the object's meta-object most-derived-first
145 /// (QML-added members, then Rust, then the C++ base). Returns `false` if
146 /// the target has been dropped or no method named `name` takes zero
147 /// arguments; returns `true` otherwise.
148 pub fn invoke_method(&self, name: &str) -> bool {
149 if !self.is_alive() {
150 return false;
151 }
152 unsafe { ffi::invoke_method(self.obj, name, &QVariantList::default()) }
153 }
154
155 /// Schedules `name` to run on the Qt thread via the Qt event loop,
156 /// passing `args` to the method.
157 ///
158 /// `name` is resolved over the object's meta-object most-derived-first
159 /// (QML-added members, then Rust, then the C++ base), choosing the first
160 /// candidate whose parameter count matches `args` and whose arguments are
161 /// all convertible to the parameter types. Returns `false` if the target
162 /// has been dropped, no candidate matches, or a conversion fails; returns
163 /// `true` otherwise.
164 pub fn invoke_method_with_args(&self, name: &str, args: &QVariantList) -> bool {
165 if !self.is_alive() {
166 return false;
167 }
168 unsafe { ffi::invoke_method(self.obj, name, args) }
169 }
170}