KDAB / cxx-qt

Safe interop between Rust and Qt
https://kdab.github.io/cxx-qt/book/
1.05k stars 75 forks source link

Deriving from a cxx-qt QObject child #1034

Closed ahayzen-kdab closed 1 month ago

ahayzen-kdab commented 2 months ago

Discussed in https://github.com/KDAB/cxx-qt/discussions/1031

Originally posted by **SanderVocke** August 13, 2024 Hi there, Another day, another question from my side. I am trying to have inheritance between my cxx-qt objects. In my particular case, I derive a class from QQuickItem, then I want another class to derive from this class. The code is at the bottom of my message. The problem I am running into is that it seems all cxx-qt generated items derive from `rust::cxxqtlib1::CxxQtLocking`, and trying to derive a new cxx-qt class from an existing one causes multiple inheritance errors: ``` C:\dev\shoopdaloop\build\cp312-cp312-win_amd64\cargo\build\x86_64-pc-windows-msvc\debug\build\shoop_rs_frontend-ea53a39f3219adf3\out\cxx-qt-gen/qobj_autoconnect.cxxqt.h(11): warning C4584: 'AutoConnect': base-class 'rust::cxxqtlib1::CxxQtLocking' is already a base-class of 'FindParentBackendWrapper' ``` I don't know if there is a way around this, and don't see any similar cases in the examples. I think not being able to do inheritance like this will put some serious constraints on code reusability. I guess one way could be to make the base class not be a Qt-derived object at all, but implement the common functionality via traits or the like. But that would not allow for e.g. inheriting properties and invokeables. Thanks in any case! The relevant code: Base class: ```rust use crate::logging::macros::*; shoop_log_unit!("Frontend.FindParentBackendWrapper"); use std::pin::Pin; #[cxx_qt::bridge] pub mod qobj_find_parent_backend_wrapper { unsafe extern "C++" { include!(); type QQuickItem; } unsafe extern "RustQt" { #[qobject] #[base = "QQuickItem"] #[qproperty(*mut QQuickItem, backend_wrapper)] #[qproperty(bool, backend_wrapper_initialized)] type FindParentBackendWrapper = super::FindParentBackendWrapperRust; } unsafe extern "C++" { include!("cxx-qt-shoop/qquickitem_parent.h"); #[rust_name = "maybe_parent_qquickitem_qquickitem_findparentbackendwrapper"] fn maybe_parent_qquickitem(item : &QQuickItem) -> *mut QQuickItem; #[rust_name = "maybe_parent_qquickitem_findparentbackendwrapper"] fn maybe_parent_qquickitem(item : &FindParentBackendWrapper) -> *mut QQuickItem; include!("cxx-qt-shoop/qobject_classname.h"); #[rust_name = "qobject_class_name_qquickitem_findparentbackendwrapper"] fn qobject_class_name(obj : &QQuickItem) -> &str; include!("cxx-qt-shoop/cast_ptr.h"); #[rust_name = "cast_ptr_findparentbackendwrapper_qquickitem"] unsafe fn cast_ptr(item : *mut FindParentBackendWrapper) -> *mut QQuickItem; } unsafe extern "RustQt" { #[qinvokable] unsafe fn rescan_parents(self: Pin<&mut FindParentBackendWrapper>); } unsafe extern "C++" { include!("cxx-qt-shoop/make_unique.h"); #[rust_name = "make_unique_findparentbackendwrapper"] fn make_unique() -> UniquePtr; } impl cxx_qt::Constructor<(*mut QQuickItem,), NewArguments=(*mut QQuickItem,)> for FindParentBackendWrapper {} impl cxx_qt::Constructor<(), NewArguments=()> for FindParentBackendWrapper {} } use qobj_find_parent_backend_wrapper::*; unsafe fn find_parent_backend(item : *mut QQuickItem) -> Result<*mut QQuickItem, String> { if item.is_null() { return Err(String::from("Could not find back-end item.")); } let is_backend = qobject_class_name_qquickitem_findparentbackendwrapper(item.as_ref().unwrap()) == "Backend"; if is_backend { return Ok(item); } let parent = maybe_parent_qquickitem_qquickitem_findparentbackendwrapper(item.as_ref().unwrap()); return find_parent_backend (parent); } pub struct FindParentBackendWrapperRust { backend_wrapper : *mut QQuickItem, backend_wrapper_initialized : bool, } impl Default for FindParentBackendWrapperRust { fn default() -> FindParentBackendWrapperRust { FindParentBackendWrapperRust { backend_wrapper : std::ptr::null_mut(), backend_wrapper_initialized : false, } } } impl FindParentBackendWrapper { pub unsafe fn rescan_parents(mut self: Pin<&mut FindParentBackendWrapper>) { let self_ptr = self.as_mut().get_unchecked_mut() as *mut FindParentBackendWrapper; let self_qquick_ptr = cast_ptr_findparentbackendwrapper_qquickitem(self_ptr); let backend = find_parent_backend(self_qquick_ptr).unwrap(); self.set_backend_wrapper(backend); } } impl cxx_qt::Constructor<(*mut QQuickItem,)> for FindParentBackendWrapper { type BaseArguments = (*mut QQuickItem,); // Will be passed to the base class constructor type InitializeArguments = (); // Will be passed to the "initialize" function type NewArguments = (*mut QQuickItem,); // Will be passed to the "new" function fn route_arguments(args: (*mut QQuickItem,)) -> ( Self::NewArguments, Self::BaseArguments, Self::InitializeArguments ) { (args, args, ()) } fn new(_parent : (*mut QQuickItem,)) -> FindParentBackendWrapperRust { FindParentBackendWrapperRust::default() } } impl cxx_qt::Constructor<()> for FindParentBackendWrapper { type BaseArguments = (); // Will be passed to the base class constructor type InitializeArguments = (); // Will be passed to the "initialize" function type NewArguments = (); // Will be passed to the "new" function fn route_arguments(_args: ()) -> ( Self::NewArguments, Self::BaseArguments, Self::InitializeArguments ) { ((), (), ()) } fn new(_args: ()) -> FindParentBackendWrapperRust { FindParentBackendWrapperRust::default() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_class_name() { let obj = make_unique_findparentbackendwrapper(); unsafe { let ptr = obj.into_raw(); let quick = cast_ptr_findparentbackendwrapper_qquickitem(ptr); let classname = qobject_class_name_qquickitem_findparentbackendwrapper(quick.as_ref().unwrap()); assert_eq!(classname, "FindParentBackendWrapper"); } } } ``` Child class: ```rust use crate::logging::macros::*; shoop_log_unit!("Frontend.AutoConnect"); // use crate::cxx_qt_shoop::qobj_find_parent_backend_wrapper; #[cxx_qt::bridge] pub mod qobj_autoconnect { unsafe extern "C++" { include!(); type QQuickItem; } unsafe extern "C++" { include!("cxx-qt-gen/qobj_find_parent_backend_wrapper.cxxqt.h"); } unsafe extern "RustQt" { #[qobject] #[base = "FindParentBackendWrapper"] type AutoConnect = super::AutoConnectRust; } unsafe extern "C++" { include!("cxx-qt-shoop/make_unique.h"); #[rust_name = "make_unique_autoconnect"] fn make_unique() -> UniquePtr; } unsafe extern "C++" { include!("cxx-qt-shoop/qobject_classname.h"); #[rust_name = "qobject_class_name_autoconnect"] fn qobject_class_name(obj : &AutoConnect) -> &str; } impl cxx_qt::Constructor<(*mut QQuickItem,), NewArguments=(*mut QQuickItem,)> for AutoConnect {} impl cxx_qt::Constructor<(), NewArguments=()> for AutoConnect {} } pub struct AutoConnectRust { } impl Default for AutoConnectRust { fn default() -> AutoConnectRust { AutoConnectRust {} } } impl cxx_qt::Constructor<(*mut QQuickItem,)> for AutoConnect { type BaseArguments = (*mut QQuickItem,); // Will be passed to the base class constructor type InitializeArguments = (); // Will be passed to the "initialize" function type NewArguments = (*mut QQuickItem,); // Will be passed to the "new" function fn route_arguments(args: (*mut QQuickItem,)) -> ( Self::NewArguments, Self::BaseArguments, Self::InitializeArguments ) { (args, args, ()) } fn new(_parent : (*mut QQuickItem,)) -> AutoConnectRust { AutoConnectRust::default() } } impl cxx_qt::Constructor<()> for AutoConnect { type BaseArguments = (); // Will be passed to the base class constructor type InitializeArguments = (); // Will be passed to the "initialize" function type NewArguments = (); // Will be passed to the "new" function fn route_arguments(_args: ()) -> ( Self::NewArguments, Self::BaseArguments, Self::InitializeArguments ) { ((), (), ()) } fn new(_args: ()) -> AutoConnectRust { AutoConnectRust::default() } } #[cfg(test)] mod tests { use super::qobj_autoconnect::*; #[test] fn test_class_name() { let obj = make_unique_autoconnect(); let classname = qobject_class_name_autoconnect (obj.as_ref().unwrap()); assert_eq!(classname, "AutoConnect"); } } ```
ahayzen-kdab commented 2 months ago

Appears that we should probably template the CxxQtLocking inheritance in CxxQtThreading so that multiple inheritance can happen. See my comment on the original discussion for more info.

SanderVocke commented 2 months ago

I didn't realize that there might be anything special taking place on the Rust side of the bridge. Note that in the example above, I never once use or mention the base type in the derived type's Rust code, but only reference its name in the #[base ...] macro. My assumption is that this would only lead to C++ inheritance.

I think your suggestion of templating the "offending" base class might do the trick.

ahayzen-kdab commented 2 months ago

Right, i hope that'll solve it which would be nice.

For more insight into what is happening here with inheritance have a look at this input https://github.com/KDAB/cxx-qt/blob/9f2de71cb12e0a816653e2bdb99f4f369d27c5c4/crates/cxx-qt-gen/test_inputs/inheritance.rs#L10-L14

The ends up generating in C++ https://github.com/KDAB/cxx-qt/blob/9f2de71cb12e0a816653e2bdb99f4f369d27c5c4/crates/cxx-qt-gen/test_outputs/inheritance.h#L11-L17

Note in that example only locking is enabled and not threading.

For an example with threading enabled like https://github.com/KDAB/cxx-qt/blob/9f2de71cb12e0a816653e2bdb99f4f369d27c5c4/crates/cxx-qt-gen/test_inputs/invokables.rs#L58

it ends up like this https://github.com/KDAB/cxx-qt/blob/9f2de71cb12e0a816653e2bdb99f4f369d27c5c4/crates/cxx-qt-gen/test_outputs/invokables.h#L16-L22

I hope that by changing CxxQtLocking -> CxxQtLocking<T> should "solve" this.

BenFordTytherington commented 2 months ago

After chasing down some of those errors, you eventually hit errors like this:

rc/ffi.cxx.cpp:315:83: error: reference to ‘unsafeRust’ is ambiguous
  cargo:warning=  315 |   ::MyObjectBRust const &(::MyObjectB::*cxx_qt_ffi_rust$)() const = &::MyObjectB::unsafeRust;
  cargo:warning=      |                                                                                   ^~~~~~~~~~
  cargo:warning=In file included from /home/ben/cxx-qt/target/debug/build/qml-minimal-no-cmake-2e43c9623c960347/out/cxx-qt-build/target/crates/qml-minimal-no-cmake/include/qml-minimal-no-cmake/ffi.cxxqt.h:5,
  cargo:warning=                 from /home/ben/cxx-qt/target/debug/build/qml-minimal-no-cmake-2e43c9623c960347/out/cxx-qt-gen/src/ffi.cxx.cpp:3:
  cargo:warning=/home/ben/cxx-qt/target/debug/build/qml-minimal-no-cmake-2e43c9623c960347/out/cxx-qt-build/target/crates/qml-minimal-no-cmake/include/cxx-qt/type.h:28:12: note: candidates are: ‘const T& rust::cxxqt1::CxxQtType<T>::unsafeRust() const [with T = MyObjectBRust]’
  cargo:warning=   28 |   T const& unsafeRust() const { return *m_rustObj; }
  cargo:warning=      |            ^~~~~~~~~~
  cargo:warning=/home/ben/cxx-qt/target/debug/build/qml-minimal-no-cmake-2e43c9623c960347/out/cxx-qt-build/target/crates/qml-minimal-no-cmake/include/cxx-qt/type.h:28:12: note:                 ‘const T& rust::cxxqt1::CxxQtType<T>::unsafeRust() const [with T = MyObjectARust]’
  cargo:warning=/home/ben/cxx-qt/target/debug/build/qml-minimal-no-cmake-2e43c9623c960347/out/cxx-qt-gen/src/ffi.cxx.cpp: In function ‘MyObjectBRust* cxxbridge1$MyObjectB$cxx_qt_ffi_rust_mut(MyObjectB&)’:
  cargo:warning=/home/ben/cxx-qt/target/debug/build/qml-minimal-no-cmake-2e43c9623c960347/out/cxx-qt-gen/src/ffi.cxx.cpp:320:75: error: reference to ‘unsafeRustMut’ is ambiguous
  cargo:warning=  320 |   ::MyObjectBRust &(::MyObjectB::*cxx_qt_ffi_rust_mut$)() = &::MyObjectB::unsafeRustMut;
  cargo:warning=      |                                                                           ^~~~~~~~~~~~~
  cargo:warning=/home/ben/cxx-qt/target/debug/build/qml-minimal-no-cmake-2e43c9623c960347/out/cxx-qt-build/target/crates/qml-minimal-no-cmake/include/cxx-qt/type.h:29:6: note: candidates are: ‘T& rust::cxxqt1::CxxQtType<T>::unsafeRustMut() [with T = MyObjectBRust]’
  cargo:warning=   29 |   T& unsafeRustMut() { return *m_rustObj; }
  cargo:warning=      |      ^~~~~~~~~~~~~
  cargo:warning=/home/ben/cxx-qt/target/debug/build/qml-minimal-no-cmake-2e43c9623c960347/out/cxx-qt-build/target/crates/qml-minimal-no-cmake/include/cxx-qt/type.h:29:6: note:                 ‘T& rust::cxxqt1::CxxQtType<T>::unsafeRustMut() [with T = MyObjectARust]’

which seems to be a deeper problem

ahayzen-kdab commented 2 months ago

It appears even if the CxxQtLocking part was templated, you then hit issues with the CXX generated code. As both the base and derived class have CxxQtType which have methods like unsafeRust(), the code CXX generates is ambiguous and the C++ compiler can't figure out which call is required.

This needs some more thought of how to compose multiple classes together that are from the Rust side or whether that can be even possible with the current structure. cc @LeonMatthesKDAB

LeonMatthesKDAB commented 2 months ago

Phew, that's a complicated issue... :thinking:

First thoughts on CxxQtLocking -> CxxQtLocking<T>: This would probably solve the "already inherits from X" problem, but in a bit of a weird way. CxxQtLocking<T> would create a new super-class for each subclass down the chain. Which also means each subclass would get it's own mutex, as both CxxQtLocking<Parent> and CxxQtLocking<Child> would contain a mutex to lock. This would probably work if disambiguated correctly, but would mean we'd add multiple mutexes, which could maybe deadlock? and in any case definitely waste memory.

My hunch is that the "correct" way to go about this is to not inherit from CxxQtLocking in the first place if the base class already does. We could of course do this manually with some kind of flag on the #[qobject]. But with the state of C++ meta-programming, my gut feeling is that this should be possible to do with some kind of meta-programming magic. The C++ compiler already knows what superclasses are included in the chain, and we have std::is_base_of, which should make this possible somehow :thinking:

LeonMatthesKDAB commented 2 months ago

Does anyone have an example repo somewhere with this error so that I can reproduce this easily? :)

SanderVocke commented 2 months ago

Hi @LeonMatthesKDAB, I'll create one since I requested this. Will report it back.

SanderVocke commented 2 months ago

@LeonMatthesKDAB: https://github.com/SanderVocke/cxx-qt-rust-inheritance

LeonMatthesKDAB commented 2 months ago

Hm, as I'm taking a look at this right now, the larger issue actually seems to be CxxQtType. As I mentioned I'm pretty sure we can use meta-programming to only inherit from CxxQtLocking a single time.

The CXX bindings to the unsafeRust and unsafeRustMut methods from CxxQtType<...> are however ambigious in the generated CXX code. We could probably work around this by generating a free method with a specific name that CXX can bind to, but that would make the CxxQtType class somewhat pointless. I'll experiment with this a bit further...

LeonMatthesKDAB commented 2 months ago

I think I should be able to get this to work with some templating trickery. Will take a bit to figure out the details. Will open a PR when ready :)

LeonMatthesKDAB commented 2 months ago

So I've got a first version of this going in #1049

For now I've simply disabled inheriting from CxxQtLocking, if it's already inherited by the base class.

However, I've noticed that CxxQtThreading also inherits from Locking, which means this still creates duplicate Locking implementations.

So we need to disable it conditionally there as well. The alternative I've found is to virtually inherit from CxxQtLocking, which would solve this issue via a v-table: See also: https://stackoverflow.com/questions/27545888/is-multiple-inheritance-from-the-same-base-class-via-different-parent-classes-re

This would actually be the easiest solution for our generation, but would require a frequent vtable lookup, so it's probably not ideal. I'll try conditionally disabling first.

LeonMatthesKDAB commented 2 months ago

Hm, yet another issue with this: Apparently QML_SINGLETON doesn't work with virtual subclassing and clang :roll_eyes: (see the CI failures here: https://github.com/KDAB/cxx-qt/actions/runs/10573868714/job/29294258974?pr=1049 )

I had to resort to virtual subclassing because CxxQtThreading also inherits from CxxQtLocking, which makes it very difficult to conditionally disable the subclassing there.

This kind of calls into question whether we can get away without CxxQtLocking, which we've discussed from time to time. I've opened a Zulip discussion on this topic so we can find a solution once Andrew is back next week: https://cxx-qt.zulipchat.com/#narrow/stream/426346-dev/topic/Deprecate.20CxxQtLocking.3F/near/465360573