diff --git a/CHANGELOG.md b/CHANGELOG.md index f6504aaeb..82be164fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `unsafe impl !cxx_qt::Locking for qobject::T` to disable internal locking - `Deref` is now implemented for `qobject::T` to reach the `T` Rust struct - Support for C++ only methods by not having a `#[qinvokable]` attribute +- Ability to define a custom C++ Constructor using `cxx_qt::Constructor` +- `cxx_qt::Initialize` trait for easier default-constructor implementation ### Changed diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 1323a0123..10f386380 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -31,3 +31,4 @@ SPDX-License-Identifier: MIT OR Apache-2.0 - [Threading](./concepts/threading.md) - [Nested Objects](./concepts/nested_objects.md) - [Inheritance & Overriding](./concepts/inheritance.md) + - [Custom Constructors](./concepts/constructor.md) diff --git a/book/src/concepts/constructor.md b/book/src/concepts/constructor.md new file mode 100644 index 000000000..c8f84bb35 --- /dev/null +++ b/book/src/concepts/constructor.md @@ -0,0 +1,118 @@ + + +# Defining a custom C++/QML constructor + +By default, CXX-Qt will generate a constructor that takes a single `QObject` pointer as optional argument (usually the QObject parent). + +It then: +1. Calls the base class constructor with the optional parent pointer. +2. uses [Rust's default trait][default-trait] to construct the inner Rust struct. + +For most cases this is good enough, however there are a few cases in which this will not be sufficient, for example: +* the base class constructor expects arguments other than the parent QObject pointer, or a different type than QObject is used for the parent (for example QQuickItem's constructor takes a QQuickItem pointer). +* the QObject needs to run code after it is initialized (e.g. open a connection to a server, etc.). +* the Rust struct can't have a `default` implementation because it needs certain arguments for construction. + +To facilitate these use-cases CXX-Qt provides the [`Constructor` trait][constructor-trait]. + +## Implementing & Declaring a Constructor +In order for CXX-Qt to generate a custom constructor, the `qobject::T` must implement the [`cxx_qt::Constructor` trait][constructor-trait]. +Additionally, the constructor must be declared within the [cxx_qt::bridge](./bridge.md). + +This declaration uses Rust's `impl ... for` syntax, but with a few special rules: +* The implementation block must be empty +* If any of the associated types in the constructor are not `()`, they + must be listed using `Type=(...)` in the constructor generics. + +Example: +```rust,ignore,noplayground +#[cxx_qt::bridge] +mod qobject { +{{#include ../../../examples/qml_features/rust/src/signals.rs:book_signals_struct}} + +{{#include ../../../examples/qml_features/rust/src/signals.rs:book_constructor_decl}} +} +// Don't forget to actually implement the trait **outside** the bridge! +``` +[Full example](https://github.com/KDAB/cxx-qt/blob/main/examples/qml_features/rust/src/signals.rs) + +## Routing arguments of the Constructor +A C++ constructor is more complex than a Rust struct initialization. + +Like a normal function, it takes a list of arguments. +However, before the actual code inside the constructor is executed, C++ ensures the following: +* The base class is constructed +* All members of the class or struct are initialized (for CXX-Qt, this includes the underlying Rust struct). + +Both of these steps may need any of the arguments given to the constructor. +The code that is actually run *inside* the constructor may also need to access some of the arguments. + +`cxx_qt::Constructor` allows controlling how the arguments are passed around in the constructor using the `route_arguments` function. +For this it uses three associated types as well as one generic type: +* Constructor generic `Arguments` - argument list that the constructor takes. +* `type BaseArguments` - argument list that is passed to the base class constructor. +* `type NewArguments` - argument list used to construct the Rust struct using the `new` function. +* `type InitializeArguments` - argument list that is passed to the code run within the constructor (the `initialize` function). + +These types must all be tuple types and can therefore support an arbitrary list of arguments. + +The `route_arguments` function takes the Arguments provided to the constructor and distributes them to the three different types in the above list. +Doing this in Rust allows calling any base class constructor and controlling movement, copying or other operations on the arguments. +Every constructor defined using `cxx_qt::Constructor` will execute this function once, before anything else in the Constructor happens and then distributes the resultant values to the appropriate steps of the QObject construction. + +As actual construction takes place in C++, all types used must be compatible with CXX so they can pass the FFI barrier. + +## Constructing the Rust struct with arguments +If the construction of the inner Rust struct requires some arguments, the `new` function of the Constructor can be given arguments by using the `NewArguments` associated type. + +Example: +```rust,ignore +#[cxx_qt::bridge] +mod qobject { + // ... + +{{#include ../../../examples/qml_features/rust/src/properties.rs:book_constructor_new_decl}} +} + +{{#include ../../../examples/qml_features/rust/src/properties.rs:book_constructor_new_impl}} +``` + +## Initializing the QObject +In addition to running code before constructing the inner Rust struct, it may be useful to run code from the context of the QObject itself (i.e. inside the Constructor implementation). + +The `initialize` function can be used to run code inside a constructor. +It is given a pinned mutable self reference to the QObject and the list of `InitializeArguments`. + +### Using the `Initialize` trait +The QML engine creates QML elements using their default constructor, so for most QML types only the `initialize` part of the constructor is of interest. +To reduce the boilerplate of this use-case, CXX-Qt provides the [`Initialize`][initialize-trait] trait. + +If a QObject implements the `Initialize` trait, and the inner Rust struct is [Default][default-trait]-constructible it will automatically implement `cxx_qt::Constructor<()>`. + +Example: +```rust,ignore +{{#include ../../../examples/qml_features/rust/src/signals.rs:book_initialize_impl}} + // ... + } +} +``` +[Full example](https://github.com/KDAB/cxx-qt/blob/main/examples/qml_features/rust/src/signals.rs) + +Then just remember to declare the `cxx_qt::Constructor<()>` inside the `cxx_qt::bridge`. +```rust,ignore +#[cxx_qt::bridge] +mod qobject { +{{#include ../../../examples/qml_features/rust/src/signals.rs:book_signals_struct}} + +{{#include ../../../examples/qml_features/rust/src/signals.rs:book_initialize_decl}} +} +``` + +[constructor-trait]: https://docs.rs/cxx-qt/latest/cxx_qt/trait.Constructor.html +[initialize-trait]: https://docs.rs/cxx-qt/latest/cxx_qt/trait.Initialize.html +[default-trait]: https://doc.rust-lang.org/std/default/trait.Default.html diff --git a/book/src/concepts/index.md b/book/src/concepts/index.md index 0281f58ea..b8c10508f 100644 --- a/book/src/concepts/index.md +++ b/book/src/concepts/index.md @@ -23,3 +23,4 @@ SPDX-License-Identifier: MIT OR Apache-2.0 * [Threading concept and safety](./threading.md) * [Nesting Rust objects](./nested_objects.md) * [Inheriting QObjects and overriding methods](./inheritance.md) + * [Defining custom C++/QML Constructors](./constructor.md) diff --git a/book/src/concepts/inheritance.md b/book/src/concepts/inheritance.md index 568714650..fd224d68c 100644 --- a/book/src/concepts/inheritance.md +++ b/book/src/concepts/inheritance.md @@ -11,6 +11,9 @@ Some Qt APIs require you to override certain methods from an abstract base class To support creating such subclasses directly from within Rust, CXX-Qt provides you with multiple helpers. +Some superclasses may require special parameters for construction. +This can be achieved by using a [custom constructor](./constructor.md). + ## Accessing base class methods To access the methods of a base class in Rust, use the `#[inherit]` macro. It can be placed in front of a function in a `extern "RustQt"` block in a `#[cxx_qt::bridge]`. diff --git a/crates/cxx-qt/src/lib.rs b/crates/cxx-qt/src/lib.rs index 85d70c1e7..6c3558842 100644 --- a/crates/cxx-qt/src/lib.rs +++ b/crates/cxx-qt/src/lib.rs @@ -193,3 +193,45 @@ pub trait Constructor: CxxQtType { // By default, do nothing } } + +/// This trait can be implemented on any [CxxQtType] to automatically define a default constructor +/// that calls the `initialize` function after constructing a default Rust struct. +/// +/// The default constructor will have the following signature: +/// ```ignore +/// cxx_qt::Constructor<()> +/// ``` +// TODO: Once the QObject type is available in the cxx-qt crate, also auto-generate a default +// constructor that takes QObject and passes it to the parent. +pub trait Initialize: CxxQtType { + /// This function is called to initialize the QObject after construction. + fn initialize(self: core::pin::Pin<&mut Self>); +} + +impl Constructor<()> for T +where + T: Initialize, + T::Rust: Default, +{ + type NewArguments = (); + type BaseArguments = (); + type InitializeArguments = (); + + fn new(_arguments: ()) -> ::Rust { + ::Rust::default() + } + + fn route_arguments( + _arguments: (), + ) -> ( + Self::NewArguments, + Self::BaseArguments, + Self::InitializeArguments, + ) { + ((), (), ()) + } + + fn initialize(self: core::pin::Pin<&mut Self>, _arguments: Self::InitializeArguments) { + Self::initialize(self); + } +} diff --git a/examples/qml_features/rust/src/custom_parent_class.rs b/examples/qml_features/rust/src/custom_parent_class.rs index 0508a53e1..a75fcf438 100644 --- a/examples/qml_features/rust/src/custom_parent_class.rs +++ b/examples/qml_features/rust/src/custom_parent_class.rs @@ -103,26 +103,8 @@ impl qobject::CustomParentClass { } } -impl cxx_qt::Constructor<()> for qobject::CustomParentClass { - type NewArguments = (); - type BaseArguments = (*mut qobject::QQuickItem,); - type InitializeArguments = (); - - fn route_arguments( - _args: (), - ) -> ( - Self::NewArguments, - Self::BaseArguments, - Self::InitializeArguments, - ) { - ((), (core::ptr::null_mut(),), ()) - } - - fn new((): ()) -> CustomParentClassRust { - CustomParentClassRust::default() - } - - fn initialize(self: core::pin::Pin<&mut Self>, _arguments: Self::InitializeArguments) { +impl cxx_qt::Initialize for qobject::CustomParentClass { + fn initialize(self: core::pin::Pin<&mut Self>) { self.on_color_changed(|qobject| qobject.update()).release(); } } diff --git a/examples/qml_features/rust/src/nested_qobjects.rs b/examples/qml_features/rust/src/nested_qobjects.rs index 665edb498..3be0d9a80 100644 --- a/examples/qml_features/rust/src/nested_qobjects.rs +++ b/examples/qml_features/rust/src/nested_qobjects.rs @@ -98,27 +98,9 @@ impl qobject::OuterObject { } } -impl cxx_qt::Constructor<()> for qobject::OuterObject { - type InitializeArguments = (); - type NewArguments = (); - type BaseArguments = (); - - fn route_arguments( - _: (), - ) -> ( - Self::NewArguments, - Self::BaseArguments, - Self::InitializeArguments, - ) { - ((), (), ()) - } - - fn new(_: Self::NewArguments) -> ::Rust { - Default::default() - } - - /// Initialise the QObject, creating a connection from one signal to another - fn initialize(self: core::pin::Pin<&mut Self>, _: Self::InitializeArguments) { +impl cxx_qt::Initialize for qobject::OuterObject { + /// Initialize the QObject, creating a connection from one signal to another + fn initialize(self: core::pin::Pin<&mut Self>) { // Example of connecting a signal from one QObject to another QObject // this causes OuterObject::Called to trigger InnerObject::Called self.on_called(|qobject, obj| { diff --git a/examples/qml_features/rust/src/properties.rs b/examples/qml_features/rust/src/properties.rs index ef8812d3f..3837f3f4c 100644 --- a/examples/qml_features/rust/src/properties.rs +++ b/examples/qml_features/rust/src/properties.rs @@ -36,6 +36,19 @@ pub mod qobject { #[qinvokable] fn disconnect(self: Pin<&mut RustProperties>); } + + impl cxx_qt::Constructor<()> for RustProperties {} + + // Dummy constructor, added for an example in the book. + // ANCHOR: book_constructor_new_decl + impl<'a> + cxx_qt::Constructor< + (bool, &'a QUrl, &'a QUrl, &'a QString), + NewArguments = (bool, &'a QUrl, &'a QUrl, &'a QString), + > for RustProperties + { + } + // ANCHOR_END: book_constructor_new_decl } use core::pin::Pin; @@ -102,3 +115,38 @@ impl qobject::RustProperties { self.set_connected_url(QUrl::default()); } } + +impl cxx_qt::Initialize for qobject::RustProperties { + fn initialize(self: Pin<&mut Self>) {} +} + +// Dummy constructor, added for an example in the book. +// ANCHOR: book_constructor_new_impl +impl<'a> cxx_qt::Constructor<(bool, &'a QUrl, &'a QUrl, &'a QString)> for qobject::RustProperties { + type NewArguments = (bool, &'a QUrl, &'a QUrl, &'a QString); + type InitializeArguments = (); + type BaseArguments = (); + + fn route_arguments( + arguments: (bool, &'a QUrl, &'a QUrl, &'a QString), + ) -> ( + Self::NewArguments, + Self::BaseArguments, + Self::InitializeArguments, + ) { + // pass all arguments to the `new` function. + (arguments, (), ()) + } + + fn new( + (connected, connected_url, previous_connected_url, status_message): Self::NewArguments, + ) -> Self::Rust { + Self::Rust { + connected, + connected_url: connected_url.clone(), + previous_connected_url: previous_connected_url.clone(), + status_message: status_message.clone(), + } + } +} +// ANCHOR_END: book_constructor_new_impl diff --git a/examples/qml_features/rust/src/signals.rs b/examples/qml_features/rust/src/signals.rs index 9290188a0..1289ea8b0 100644 --- a/examples/qml_features/rust/src/signals.rs +++ b/examples/qml_features/rust/src/signals.rs @@ -34,13 +34,14 @@ pub mod qobject { } // ANCHOR_END: book_signals_block + // ANCHOR: book_signals_struct unsafe extern "RustQt" { - // ANCHOR: book_signals_struct #[qobject] #[qml_element] #[qproperty(bool, logging_enabled)] type RustSignals = super::RustSignalsRust; } + // ANCHOR_END: book_signals_struct // ANCHOR: book_rust_obj_impl unsafe extern "RustQt" { @@ -54,9 +55,13 @@ pub mod qobject { } // ANCHOR_END: book_rust_obj_impl + // ANCHOR: book_initialize_decl impl cxx_qt::Constructor<()> for RustSignals {} + // ANCHOR_END: book_initialize_decl + // ANCHOR: book_constructor_decl impl<'a> cxx_qt::Constructor<(&'a QUrl,), InitializeArguments = (&'a QUrl,)> for RustSignals {} + // ANCHOR_END: book_constructor_decl } use core::pin::Pin; @@ -91,27 +96,11 @@ impl qobject::RustSignals { } } -impl cxx_qt::Constructor<()> for qobject::RustSignals { - type BaseArguments = (); - type NewArguments = (); - type InitializeArguments = (); - - fn route_arguments( - _: (), - ) -> ( - Self::NewArguments, - Self::BaseArguments, - Self::InitializeArguments, - ) { - ((), (), ()) - } - - fn new(_: Self::NewArguments) -> ::Rust { - Default::default() - } - - /// Initialise the QObject, creating a connection reacting to the logging enabled property - fn initialize(self: core::pin::Pin<&mut Self>, _: Self::InitializeArguments) { +// ANCHOR: book_initialize_impl +impl cxx_qt::Initialize for qobject::RustSignals { + /// Initialize the QObject, creating a connection reacting to the logging enabled property + fn initialize(self: core::pin::Pin<&mut Self>) { + // ANCHOR_END: book_initialize_impl self.on_logging_enabled_changed(|mut qobject| { // Determine if logging is enabled if *qobject.as_ref().logging_enabled() {