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;
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}