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