Skip to content

Commit

Permalink
Implement cxx_qt::inherit! to allow inheriting methods from super cla…
Browse files Browse the repository at this point in the history
…ss (#363)

* Parse cxx_qt::inherit! into ParsedInheritedMethod

* cxx_qt::inherit! - add Rust generation

* CombinedIdent: Revert From<Ident> implementation

Use associated function `from_rust_function` instead.

* cxx_qt::inherit: add rust generation

* Allow cxx_qt::inherit on qobjects without base.

Base class defaults to `QObject`.
  • Loading branch information
LeonMatthesKDAB authored Feb 23, 2023
1 parent bf94b05 commit 0f8d04c
Show file tree
Hide file tree
Showing 39 changed files with 1,753 additions and 190 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Added

- Support for inheriting methods from the superclass into Rust using `#[cxx_qt::inherit]`.
- Register QML types at build time: `#[cxxqt::qobject(qml_uri = "foo.bar", qml_version = "1.0")]`
- Register QRC resources at build time in Cargo builds (don't need to call initialization function from Rust `main` function)
- Support for container types: `QSet<T>`, `QHash<K, V>`, `QList<T>`, `QMap<K, V>`, `QVector<T>`
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
- [Build Systems](./concepts/build_systems.md)
- [Threading](./concepts/threading.md)
- [Nested Objects](./concepts/nested_objects.md)
- [Inheritance & Overriding](./concepts/inheritance.md)
2 changes: 2 additions & 0 deletions book/src/concepts/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!--
SPDX-FileCopyrightText: 2021 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
SPDX-FileContributor: Andrew Hayzen <[email protected]>
SPDX-FileContributor: Leon Matthes <[email protected]>
SPDX-License-Identifier: MIT OR Apache-2.0
-->
Expand All @@ -21,3 +22,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)
74 changes: 74 additions & 0 deletions book/src/concepts/inheritance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<!--
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
-->

# Inheritance

Some Qt APIs require you to override certain methods from an abstract base class, for example [QAbstractItemModel](https://doc.qt.io/qt-6/qabstractitemmodel.html).

To support creating such subclasses directly from within Rust, CXX-Qt provides you with multiple helpers.

## Accessing base class methods
To access the methods of a base class in Rust, use the `#[cxx_qt::inherit]` macro.
It can be placed in front of an `extern "C++"` block in a `#[cxx_qt::bridge]`.

```rust,ignore
{{#include ../../../examples/qml_features/rust/src/custom_base_class.rs:book_inherit_qalm}}
{{#include ../../../examples/qml_features/rust/src/custom_base_class.rs:book_inherit_qalm_impl_unsafe}}
impl qobject::CustomBaseClass {
{{#include ../../../examples/qml_features/rust/src/custom_base_class.rs:book_inherit_clear}}
}
```
[Full example](https://github.com/KDAB/cxx-qt/blob/main/examples/qml_features/rust/src/custom_base_class.rs)

This code implements a QAbstractListModel subclass.
For this, the `clear` method implemented in Rust needs to call `beginResetModel` and related methods from the base class, which are made accessible by using `#[cxx_qt::inherit]`.
See [the Qt docs](https://doc.qt.io/qt-6/qabstractlistmodel.html) for more details on the specific subclassing requirements.

Methods can be declared inside `#[cxx_qt::inherit]` in `extern "C++"` blocks similar to CXX, with the same restrictions regarding which types can be used.
Additionally, the `self` type must be either `self: Pin<&mut qobject::T>` or `self: &qobject::T`, where `qobject::T` must refer to a QObject marked with `#[cxx_qt::qobject]` in the `#[cxx_qt::bridge]`

The declared methods will be case-converted as in other CXX-Qt APIs.
To explicitly declare the C++ method name, use the `#[cxx_name="myFunctionName"]` attribute.

## Overriding base class methods

CXX-Qt allows invokables to be generated with the C++ modifiers necessary to implement inheritance.
This way methods can be overridden, declared as `virtual` or `final`.

| C++ keyword | CXX-Qt attribute |
|-------------|-------------------------------|
| `override` | `#[qinvokable(cxx_override)]` |
| `virtual` | `#[qinvokable(cxx_virtual)]` |
| `final` | `#[qinvokable(cxx_final)]` |

The below example overrides the [`data`](https://doc.qt.io/qt-6/qabstractitemmodel.html#data) method inherited from the QAbstractListModel.
```rust,ignore
{{#include ../../../examples/qml_features/rust/src/custom_base_class.rs:book_inherit_qalm}}
impl qobject::CustomBaseClass {
{{#include ../../../examples/qml_features/rust/src/custom_base_class.rs:book_inherit_data}}
}
```
[Full example](https://github.com/KDAB/cxx-qt/blob/main/examples/qml_features/rust/src/custom_base_class.rs)

When a method is overridden using `cxx_override`, the base class version of the method can be accessed by using `#[cxx_qt::inherit]` in combination with the `#[cxx_name]` attribute.
In this case the base class version of the function must get a different name because Rust can't have two functions with the same name on one type.

Example:
```rust,ignore
{{#include ../../../examples/qml_features/rust/src/custom_base_class.rs:book_inherit_qalm}}
{{#include ../../../examples/qml_features/rust/src/custom_base_class.rs:book_inherit_qalm_impl_safe}}
impl qobject::CustomBaseClass {
{{#include ../../../examples/qml_features/rust/src/custom_base_class.rs:book_inherit_can_fetch_more}}
}
```
[Full example](https://github.com/KDAB/cxx-qt/blob/main/examples/qml_features/rust/src/custom_base_class.rs)
152 changes: 152 additions & 0 deletions crates/cxx-qt-gen/src/generator/cpp/inherit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// 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

use indoc::formatdoc;

use crate::{
generator::cpp::{fragment::CppFragment, qobject::GeneratedCppQObjectBlocks},
parser::{cxxqtdata::ParsedCxxMappings, inherit::ParsedInheritedMethod},
};

use syn::{Result, ReturnType};

use super::types::CppType;

pub fn generate(
inherited_methods: &[ParsedInheritedMethod],
base_class: &Option<String>,
cxx_mappings: &ParsedCxxMappings,
) -> Result<GeneratedCppQObjectBlocks> {
let mut result = GeneratedCppQObjectBlocks::default();

for method in inherited_methods {
let return_type = if let ReturnType::Type(_, ty) = &method.method.sig.output {
CppType::from(ty, &None, cxx_mappings)?
.as_cxx_ty()
.to_owned()
} else {
"void".to_owned()
};

let base_class = base_class.as_deref().unwrap_or("QObject");

result.methods.push(CppFragment::Header(formatdoc! {
r#"
template <class... Args>
{return_type} {wrapper_ident}(Args ...args){mutability}
{{
return {base_class}::{func_ident}(args...);
}}"#,
mutability = if method.mutable { "" } else { " const" },
func_ident = method.ident.cpp,
wrapper_ident = method.wrapper_ident(),
return_type = return_type,
base_class = base_class
}));
}

Ok(result)
}

#[cfg(test)]
mod tests {
use pretty_assertions::assert_str_eq;
use syn::ForeignItemFn;

use crate::{
parser::inherit::ParsedInheritedMethod, syntax::safety::Safety, tests::tokens_to_syn,
};

use super::*;
use quote::quote;

fn generate_from_foreign(
tokens: proc_macro2::TokenStream,
base_class: Option<&str>,
) -> Result<GeneratedCppQObjectBlocks> {
let method: ForeignItemFn = tokens_to_syn(tokens);
let inherited_methods = vec![ParsedInheritedMethod::parse(method, Safety::Safe).unwrap()];
let base_class = base_class.map(|s| s.to_owned());
generate(
&inherited_methods,
&base_class,
&ParsedCxxMappings::default(),
)
}

fn assert_generated_eq(expected: &str, generated: &GeneratedCppQObjectBlocks) {
assert_eq!(generated.methods.len(), 1);
if let CppFragment::Header(header) = &generated.methods[0] {
assert_str_eq!(header, expected);
} else {
panic!("Expected header fragment");
}
}

#[test]
fn test_immutable() {
let generated = generate_from_foreign(
quote! {
fn test(self: &qobject::T, a: B, b: C);
},
Some("TestBaseClass"),
)
.unwrap();

assert_generated_eq(
indoc::indoc! {"
template <class... Args>
void testCxxQtInherit(Args ...args) const
{
return TestBaseClass::test(args...);
}"
},
&generated,
);
}

#[test]
fn test_mutable() {
let generated = generate_from_foreign(
quote! {
fn test(self: Pin<&mut qobject::T>);
},
Some("TestBaseClass"),
)
.unwrap();

assert_generated_eq(
indoc::indoc! {"
template <class... Args>
void testCxxQtInherit(Args ...args)
{
return TestBaseClass::test(args...);
}"
},
&generated,
);
}

#[test]
fn test_default_base_class() {
let generated = generate_from_foreign(
quote! {
fn test(self: &qobject::T);
},
None,
)
.unwrap();

assert_generated_eq(
indoc::indoc! {"
template <class... Args>
void testCxxQtInherit(Args ...args) const
{
return QObject::test(args...);
}"
},
&generated,
);
}
}
1 change: 1 addition & 0 deletions crates/cxx-qt-gen/src/generator/cpp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// SPDX-License-Identifier: MIT OR Apache-2.0

pub mod fragment;
pub mod inherit;
pub mod invokable;
pub mod property;
pub mod qobject;
Expand Down
7 changes: 6 additions & 1 deletion crates/cxx-qt-gen/src/generator/cpp/qobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

use crate::generator::{
cpp::{
fragment::CppFragment, invokable::generate_cpp_invokables,
fragment::CppFragment, inherit, invokable::generate_cpp_invokables,
property::generate_cpp_properties, signal::generate_cpp_signals,
},
naming::{namespace::NamespaceName, qobject::QObjectName},
Expand Down Expand Up @@ -107,6 +107,11 @@ impl GeneratedCppQObject {
cxx_mappings,
)?);
}
generated.blocks.append(&mut inherit::generate(
&qobject.inherited_methods,
&qobject.base_class,
cxx_mappings,
)?);

Ok(generated)
}
Expand Down
42 changes: 42 additions & 0 deletions crates/cxx-qt-gen/src/generator/naming/functions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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

use convert_case::{Case, Casing};
use quote::format_ident;
use syn::Ident;

use super::CombinedIdent;

impl CombinedIdent {
/// Generate a CombinedIdent from a rust function name.
/// C++ will use the CamelCase version of the function name.
pub fn from_rust_function(ident: Ident) -> Self {
Self {
cpp: format_ident!("{}", ident.to_string().to_case(Case::Camel)),
rust: ident,
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_from_rust_function_camel_case_conversion() {
let ident = format_ident!("test_function");
let combined = CombinedIdent::from_rust_function(ident.clone());
assert_eq!(combined.cpp, format_ident!("testFunction"));
assert_eq!(combined.rust, ident);
}

#[test]
fn test_from_rust_function_single_word() {
let ident = format_ident!("test");
let combined = CombinedIdent::from_rust_function(ident.clone());
assert_eq!(combined.cpp, ident);
assert_eq!(combined.rust, ident);
}
}
26 changes: 10 additions & 16 deletions crates/cxx-qt-gen/src/generator/naming/invokable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,20 @@ impl From<&ImplItemMethod> for QInvokableName {
fn from(method: &ImplItemMethod) -> Self {
let ident = &method.sig.ident;
Self {
name: name_from_ident(ident),
wrapper: wrapper_from_ident(ident),
name: CombinedIdent::from_rust_function(ident.clone()),
wrapper: CombinedIdent::wrapper_from_invokable(ident),
}
}
}

/// For a given ident generate the Rust and C++ names
fn name_from_ident(ident: &Ident) -> CombinedIdent {
CombinedIdent {
cpp: format_ident!("{}", ident.to_string().to_case(Case::Camel)),
rust: ident.clone(),
}
}

/// For a given ident generate the Rust and C++ wrapper names
fn wrapper_from_ident(ident: &Ident) -> CombinedIdent {
let ident = format_ident!("{ident}_wrapper");
CombinedIdent {
cpp: format_ident!("{}", ident.to_string().to_case(Case::Camel)),
rust: ident,
impl CombinedIdent {
/// For a given ident generate the Rust and C++ wrapper names
fn wrapper_from_invokable(ident: &Ident) -> Self {
let ident = format_ident!("{ident}_wrapper");
Self {
cpp: format_ident!("{}", ident.to_string().to_case(Case::Camel)),
rust: ident,
}
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/cxx-qt-gen/src/generator/naming/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-FileContributor: Andrew Hayzen <[email protected]>
//
// SPDX-License-Identifier: MIT OR Apache-2.0
pub mod functions;
pub mod invokable;
pub mod namespace;
pub mod property;
Expand Down
Loading

0 comments on commit 0f8d04c

Please sign in to comment.