Skip to content
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

Implement cxx_qt::inherit! to allow inheriting methods from super class #363

Merged
merged 14 commits into from
Feb 23, 2023
Merged
Show file tree
Hide file tree
Changes from 12 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
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)
76 changes: 76 additions & 0 deletions book/src/concepts/inheritance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<!--
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.
Specifically [QAbstractItemModel](https://doc.qt.io/qt-6/qabstractitemmodel.html).
LeonMatthesKDAB marked this conversation as resolved.
Show resolved Hide resolved

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, access to the `beginResetModel` and related methods is required.
See [the Qt docs](https://doc.qt.io/qt-6/qabstractlistmodel.html) for more details on the specific subclassing requirements.
These methods are then made accessible by using `#[cxx_qt::inherit]`.
LeonMatthesKDAB marked this conversation as resolved.
Show resolved Hide resolved

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` may refer to any valid `#[cxx_qt::qobject]` in the `#[cxx_qt::bridge]`
LeonMatthesKDAB marked this conversation as resolved.
Show resolved Hide resolved

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)

Note that if 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.
LeonMatthesKDAB marked this conversation as resolved.
Show resolved Hide resolved
In this case the base class version of the function must get a different name, as Rust can't have two functions with the same name on one type.
LeonMatthesKDAB marked this conversation as resolved.
Show resolved Hide resolved

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)
}
LeonMatthesKDAB marked this conversation as resolved.
Show resolved Hide resolved

#[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::*;
ahayzen-kdab marked this conversation as resolved.
Show resolved Hide resolved

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()),
LeonMatthesKDAB marked this conversation as resolved.
Show resolved Hide resolved
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