Skip to main content

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}