Skip to content

Constructor documentation #639

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
118 changes: 118 additions & 0 deletions book/src/concepts/constructor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<!--
SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
SPDX-FileContributor: Leon Matthes <[email protected]>

SPDX-License-Identifier: MIT OR Apache-2.0
-->

# 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
1 change: 1 addition & 0 deletions book/src/concepts/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
3 changes: 3 additions & 0 deletions book/src/concepts/inheritance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]`.
Expand Down
42 changes: 42 additions & 0 deletions crates/cxx-qt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,45 @@ pub trait Constructor<Arguments>: 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<T> Constructor<()> for T
where
T: Initialize,
T::Rust: Default,
{
type NewArguments = ();
type BaseArguments = ();
type InitializeArguments = ();

fn new(_arguments: ()) -> <Self as CxxQtType>::Rust {
<Self as CxxQtType>::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);
}
}
22 changes: 2 additions & 20 deletions examples/qml_features/rust/src/custom_parent_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
24 changes: 3 additions & 21 deletions examples/qml_features/rust/src/nested_qobjects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) -> <Self as cxx_qt::CxxQtType>::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| {
Expand Down
48 changes: 48 additions & 0 deletions examples/qml_features/rust/src/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
33 changes: 11 additions & 22 deletions examples/qml_features/rust/src/signals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand All @@ -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;
Expand Down Expand Up @@ -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) -> <Self as CxxQtType>::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() {
Expand Down