qtbridge_runtime/registry.rs
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only
3
4//! The bridge's object index and deletion policy.
5//!
6//! Liveness is not held here: every attached object is kept alive by its
7//! proxy, so a Rust value lives exactly as long as its `QObject`, plus any
8//! user handles, which are plain `Rc<RefCell<T>>`s whose drops never
9//! tear anything down. The registry observes each object through a `Weak`
10//! reference and decides when the `QObject`s of Rust-created objects die.
11//!
12//! Every entry names its [`Owner`]:
13//!
14//! * [`Owner::RustRegistry`]: Rust-created. Pinned to `CppOwnership` while
15//! Rust holds a handle, so the QML engine cannot delete it. The engine
16//! keeps the JS wrapper of a `CppOwnership` object alive for the whole
17//! object lifetime. [`collect_garbage`] therefore hands ownership to the
18//! engine by setting `JavaScriptOwnership` for objects without Rust
19//! interest (strong count is down to the proxy's own). The engine's
20//! garbage collector deletes them with exact reachability and takes
21//! down the value together with the proxy. An object that re-enters Rust
22//! is changed back to `CppOwnership`. Objects that were never wrapped
23//! with a JS wrapper are deleted directly when Rust interest vanishes.
24//! * [`Owner::Engine`]: QML-created. The engine (or a parent) deletes the
25//! `QObject`; the registry never does, and the entry only serves the
26//! proxy lookup.
27//!
28//! The `CppOwnership` flag guards only against the garbage collector:
29//! deletion paths that ignore the ownership flag (parents, components,
30//! engine death) can take a `QObject` of either kind. The registry entry
31//! is deleted together with the `QObject` but the Rust value then survives
32//! through user handles and gets a fresh `QObject` with `Owner::RustRegistry`
33//! attached on its next exposure.
34//!
35//! [`collect_garbage`] is triggered by the garbage collection of the
36//! QmlEngine and under allocation pressure (see `register`).
37//! A garbage collection cannot be observed with e.g. connecting to a
38//! signal, so we use a sentinel that is injected into the QML engine and
39//! that should be deleted on the next garbage collector cycle.
40
41use std::any::Any;
42use std::cell::{Cell, RefCell};
43use std::collections::HashMap;
44use std::rc::Weak;
45
46use qtbridge_type_lib::QObject;
47
48#[cxx::bridge]
49mod ffi {
50 unsafe extern "C++" {
51 include!("qtbridge-type-lib/src/core/qobject/cpp/qobject.h");
52 type QObject = qtbridge_type_lib::QObject;
53
54 include!("cpp/registry.h");
55 }
56
57 unsafe extern "C++" {
58 include!("qtbridge-type-lib/src/qml/qqmlapplicationengine/cpp/qqmlapplicationengine.h");
59 type QQmlApplicationEngine = qtbridge_type_lib::QQmlApplicationEngine;
60 }
61
62 #[namespace = "rust::bridge::registry"]
63 unsafe extern "C++" {
64 /// Whether any QML engine currently holds a JS wrapper for `obj`.
65 #[rust_name = has_live_js_wrapper]
66 unsafe fn hasLiveJsWrapper(obj: *const QObject) -> bool;
67
68 #[rust_name = set_cpp_ownership]
69 unsafe fn setCppOwnership(obj: *mut QObject);
70
71 #[rust_name = set_javascript_ownership]
72 unsafe fn setJavaScriptOwnership(obj: *mut QObject);
73
74 #[rust_name = is_javascript_ownership]
75 unsafe fn isJavaScriptOwnership(obj: *mut QObject) -> bool;
76
77 #[rust_name = install_gc_sentinel_impl]
78 fn installGcSentinel(engine: Pin<&mut QQmlApplicationEngine>);
79 }
80
81 #[namespace = "rust::bridge::registry"]
82 extern "Rust" {
83 fn collect_garbage();
84 }
85}
86
87/// Arms the automatic collection trigger on `engine` by creating a sentinel.
88///
89/// The sentinel is a dummy `QObject` with `JavaScriptOwnership` whose JS
90/// wrapper is referenced by nothing: the next garbage collection frees the
91/// wrapper and thereby deletes the sentinel, whose `destroyed()` signal runs
92/// [`collect_garbage`] and re-arms a new sentinel one event-loop turn later.
93/// [`crate::QApp`] arms this automatically; call it manually when driving a raw engine.
94pub fn install_gc_sentinel(engine: core::pin::Pin<&mut qtbridge_type_lib::QQmlApplicationEngine>) {
95 ffi::install_gc_sentinel_impl(engine);
96}
97
98/// ownership indicator:
99 #[derive(Clone, PartialEq)]
100 pub enum Owner {
101 /// The registry: pinned to `CppOwnership` while Rust holds a handle,
102 /// changed to `JavaScriptOwnership` by [`collect_garbage`] and then
103 /// finally deleted by the QML engine.
104 RustRegistry,
105 /// The QML engine which deletes its own objects.
106 Engine,
107}
108
109struct Entry {
110 /// Type-erased pointer to the object's `RustProxy`.
111 proxy: *const u8,
112 /// The attached [`QObject`]. Valid for as long as the entry exists: its
113 /// deletion tears down the proxy, whose `on_drop` unregisters the entry.
114 qobject: *mut QObject,
115 /// Observe Rust usage. RustProxy holds the strong reference and
116 /// guarantees liveness.
117 value: Weak<dyn Any>,
118 /// Initiator of deletion of this entry:
119 owner: Owner,
120}
121
122/// Entries keyed by the address of the user value.
123struct Entries {
124 map: HashMap<*const u8, Entry>,
125 /// Number of entries with a `shared_owner` for debugging and
126 /// collecting under pressure
127 owned: usize,
128}
129
130impl Drop for Entries {
131 fn drop(&mut self) {
132 for (_, entry) in self.map.drain() {
133 if entry.owner == Owner::RustRegistry {
134 QObject::delete(entry.qobject); // Deletes both proxies
135 }
136 }
137 }
138}
139
140thread_local! {
141 static REGISTRY: RefCell<Entries> = RefCell::new(Entries { map: HashMap::new(), owned: 0 });
142}
143
144thread_local! {
145 static COLLECT_THRESHOLD: Cell<usize> = const { Cell::new(64) };
146}
147
148fn owned_count() -> usize {
149 REGISTRY.with_borrow(|entries| entries.owned)
150}
151
152/// Registers an attached object
153pub(crate) fn register(
154 key: *const u8, proxy: *const u8, qobject: *mut QObject,
155 value: Weak<dyn Any>, owner: Owner
156) {
157 let registry_owned = owner == Owner::RustRegistry;
158 if registry_owned {
159 unsafe { ffi::set_cpp_ownership(qobject) };
160 }
161 REGISTRY.with_borrow_mut(|entries| {
162 let old = entries.map.insert(key, Entry { proxy, qobject, value, owner });
163 debug_assert!(old.is_none(), "Object is already registered");
164 entries.owned += registry_owned as usize;
165 });
166 // If we reach a certain amount of QObjects, we will trigger a collect to
167 // clean up stale objects.
168 if registry_owned && owned_count() >= COLLECT_THRESHOLD.get() {
169 collect_garbage();
170 }
171}
172
173/// Takes ownership back when a handed-over object re-enters Rust.
174pub(crate) fn repin(key: *const u8) {
175 REGISTRY.with_borrow(|entries| {
176 if let Some(entry) = entries.map.get(&key)
177 && entry.owner == Owner::RustRegistry {
178 unsafe { ffi::set_cpp_ownership(entry.qobject) };
179 }
180 });
181}
182
183/// Drops the entry for `key`; called from the proxy teardown when the
184/// `QObject` is deleted (by [`collect_garbage`] or by the engine). Entries
185/// of objects freed by [`collect_garbage`] are already extracted by then,
186/// and during registry teardown the map is being drained.
187pub(crate) fn unregister(key: *const u8) {
188 let _ = REGISTRY.try_with(|entries: &RefCell<Entries>| {
189 let mut entries = entries.borrow_mut();
190 if let Some(entry) = entries.map.remove(&key) {
191 entries.owned -= (entry.owner == Owner::RustRegistry) as usize;
192 }
193 });
194}
195
196/// Returns the type-erased `RustProxy` pointer for `key`, or null when the
197/// object has no attached `QObject`.
198pub(crate) fn proxy_ptr(key: *const u8) -> *const u8 {
199 REGISTRY.with_borrow(|entries| {
200 entries.map.get(&key).map_or(std::ptr::null(), |entry| entry.proxy)
201 })
202}
203
204/// The number of objects the registry currently owns. Useful for leak
205/// checks.
206pub fn live_count() -> usize {
207 owned_count()
208}
209
210/// The number of objects the registry currently owns. Useful for leak
211/// checks.
212pub fn live_proxy_count() -> usize {
213 REGISTRY.with_borrow(|entries| entries.map.len())
214}
215
216
217/// Frees every object that is neither referenced from Rust nor reachable from
218/// QML.
219/// Runs automatically after every garbage collection if using [crate::QApp]
220/// and under allocation pressure; call it explicitly for deterministic
221/// reclamation points.
222pub fn collect_garbage() {
223 // Rust interest is the strong count above the registry's own reference.
224 // Objects the engine never wrapped are freed directly; wrapped ones are
225 // handed over to the engine, whose next garbage collection deletes them
226 // unless QML still reaches them.
227
228 // Freeing an object can release its references to other registered
229 // objects (e.g. children stored in fields), so iterate to a fixpoint.
230 loop {
231 // Extract first, act outside the borrow: deleting a QObject
232 // re-enters the registry through the proxy teardown's unregister.
233 let doomed: Vec<(Weak<dyn Any>, *mut QObject)> = REGISTRY.with_borrow_mut(|entries| {
234 let extracted: Vec<_> = entries.map.extract_if(|_key, entry| {
235 if entry.owner == Owner::Engine {
236 return false;
237 }
238 debug_assert!(entry.value.strong_count() >= 1);
239 if entry.value.strong_count() > 1 {
240 return false;
241 }
242 if unsafe { ffi::is_javascript_ownership(entry.qobject) } {
243 return false;
244 }
245 if unsafe { ffi::has_live_js_wrapper(entry.qobject) } {
246 // The wrapper of a CppOwned object never dies and we can
247 // therefore not track QML interest into the object. Hand
248 // the object to the engine in order to check QML interest.
249 // The engine will initiate the teardown.
250 unsafe { ffi::set_javascript_ownership(entry.qobject) };
251 return false;
252 }
253 // No Rust interest (Only strong reference is in the proxy)
254 // No QML interest (No JS Wrapper)
255 true
256 })
257 .map(|(_key, entry)| {
258 (entry.value, entry.qobject)
259 })
260 .collect();
261 entries.owned -= extracted.len();
262 extracted
263 });
264 if doomed.is_empty() {
265 break;
266 }
267 for (value, qobject) in doomed {
268 // Ensure that the Rust object is alive for the whole destructor
269 let keep_alive = value.upgrade();
270 assert!(keep_alive.is_some());
271 // Tears down the proxy pair; its on_drop removes the registry
272 // entry.
273 QObject::delete(qobject);
274 // Ours is the last reference: this frees the Rust object,
275 // running a user-provided Drop if there is one.
276 drop(keep_alive);
277 }
278 }
279 // Update the threshold on when we automatically collect QObjects.
280 // Twice the amount after a fresh sweep seems to be a good spot.
281 // The minimum value of 64 avoids collection on every few allocations.
282 // TODO: We might have to re-evaluate the values or this simplistic
283 // algorithm.
284 COLLECT_THRESHOLD.set((owned_count() * 2).max(64));
285}