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 automatic implementations of up and downcasting #1159

Merged
merged 6 commits into from
Feb 28, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- CXX-Qt-build: Multi-crate projects are now possible with Cargo and CMake (see `examples/qml_multi_crates`)
- CXX-Qt-build: Allow forcing initialization of crates/QML modules (`cxx_qt::init_crate!`/`cxx_qt::init_qml_module!`)
- Add pure virtual function specified through the `#[cxx_pure]` attribute
- Add wrappers for up and down casting, for all types which inherit from QObject, available for &T, &mut T and Pin<&mut T>

### Fixed

Expand Down
1 change: 1 addition & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ SPDX-License-Identifier: MIT OR Apache-2.0
- [Types](./concepts/types.md)
- [Nested Objects](./concepts/nested_objects.md)
- [Inheritance & Overriding](./concepts/inheritance.md)
- [Casting](./concepts/casting.md)
- [Reference: the bridge module](./bridge/index.md)
- [`extern "RustQt"`](./bridge/extern_rustqt.md)
- [`extern "C++Qt"`](./bridge/extern_cppqt.md)
Expand Down
2 changes: 1 addition & 1 deletion book/src/bridge/extern_rustqt.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Use the CXX `include!` macro to include the appropriate C++ header for the base
{{#include ../../../examples/qml_features/rust/src/custom_base_class.rs:book_base_include}}
```

For more information on inheritance and how to override methods see the [Inheritance & Overriding](../concepts/inheritance.md) page.
For more information on inheritance and how to override methods see the [Inheritance & Overriding](../concepts/inheritance.md) page and the [Casting](../concepts/casting.md) page.

[Full Example](https://github.com/KDAB/cxx-qt/blob/main/examples/qml_features/rust/src/custom_base_class.rs)

Expand Down
5 changes: 5 additions & 0 deletions book/src/bridge/traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,8 @@ For further documentation, refer to the documentation of the individual traits:
- [Constructor](https://docs.rs/cxx-qt/latest/cxx_qt/trait.Constructor.html) - custom constructor
- [Initialize](https://docs.rs/cxx-qt/latest/cxx_qt/trait.Initialize.html) - execute Rust code when the object is constructed
- [Threading](https://docs.rs/cxx-qt/latest/cxx_qt/trait.Threading.html) - marker trait whether CXX-Qt threading should be enabled

> ⚠️ These traits should only be implemented if you are sure you need to, they are automatically implemented for RustQt types.

- [Upcast](https://docs.rs/cxx-qt/latest/cxx_qt/trait.Upcast.html) - Allows a type to access its parent class if there is one
- [Downcast](https://docs.rs/cxx-qt/latest/cxx_qt/trait.Downcast.html) - Allows a type to access its child class if there is one
42 changes: 42 additions & 0 deletions book/src/concepts/casting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!--
SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
SPDX-FileContributor: Ben Ford <[email protected]>

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

# Casting

With the [base](../bridge/attributes.md) attribute, it is possible to inherit from another type.
In order to access this parent class, we provide an API to cast up or down.
Currently, this is only supported for objects in `extern "RustQt"` blocks, which have either a `#[qobject]` attribute,
or a `#[base = T]` attribute. see [here](../bridge/attributes.md) for more details on these attributes.

## Accessing the base class

To access the methods of a base class in Rust, use the `Upcast` trait like so `use cxx_qt::Upcast;`.
Objects with base classes can then be accessed with the following methods

| Self Type | Method |
|------------------|----------------|
| `&self` | `upcast()` |
| `&mut self` | `upcast_mut()` |
| `Pin<&mut self>` | `upcast_pin()` |

This will then return a reference to the base in the same format as the self type, e.g. `upcast()` returns `&Base`, etc...

## Accessing the child class

This also works in the opposite direction, allowing access to the child a base class was obtained from.
To do this, use the `Downcast` trait like so `use cxx_qt::Downcast;`.
The child can then be accessed in the same manner, with the following methods

| Self Type | Method |
|------------------|------------------|
| `&self` | `downcast()` |
| `&mut self` | `downcast_mut()` |
| `Pin<&mut self>` | `downcast_pin()` |

These will return an `Option<T>`, as it is possible that downcasting will fail,
if the type is not actually of the given subclass,
and these also return in the same format as the self type, e.g. `downcast()` returns `Option<&Sub>`, etc...
6 changes: 6 additions & 0 deletions crates/cxx-qt-gen/src/generator/cpp/qobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ impl GeneratedCppQObject {
class_initializers.push(initializer);
}

// Include casting header
let mut result = GeneratedCppQObjectBlocks::default();
result.includes.insert("#include <cxx-qt/casting.h>".into());

generated.blocks.append(&mut result);

generated.blocks.append(&mut constructor::generate(
&generated,
&structured_qobject.constructors,
Expand Down
33 changes: 29 additions & 4 deletions crates/cxx-qt-gen/src/generator/rust/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub mod signals;
pub mod threading;

use crate::generator::{rust::fragment::GeneratedRustFragment, structuring};
use crate::parser::{parameter::ParsedFunctionParameter, Parser};
use crate::parser::{parameter::ParsedFunctionParameter, qobject::ParsedQObject, Parser};
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::{parse_quote, Item, ItemMod, Result};
Expand Down Expand Up @@ -60,6 +60,8 @@ impl GeneratedRustBlocks {
let namespace = parser.cxx_qt_data.namespace.clone().unwrap_or_default();
let passthrough_mod = &parser.passthrough_module;

fragments.extend(vec![add_qobject_import(&parser.cxx_qt_data.qobjects)]);

let vis = &passthrough_mod.vis;
let ident = &passthrough_mod.module_ident;
let docs = &passthrough_mod.docs;
Expand Down Expand Up @@ -95,6 +97,29 @@ impl GeneratedRustBlocks {
}
}

fn add_qobject_import(qobjects: &[ParsedQObject]) -> GeneratedRustFragment {
let includes = qobjects
.iter()
.any(|obj| obj.has_qobject_macro && obj.base_class.is_none());
if includes {
GeneratedRustFragment {
cxx_mod_contents: vec![parse_quote! {
extern "C++" {
#[doc(hidden)]
#[namespace=""]
type QObject = cxx_qt::QObject;
}
}],
cxx_qt_mod_contents: vec![],
}
} else {
GeneratedRustFragment {
cxx_mod_contents: vec![],
cxx_qt_mod_contents: vec![],
}
}
}

/// Return the [TokenStream] of the parsed parameters for use in generation
pub fn get_params_tokens(
mutable: bool,
Expand Down Expand Up @@ -143,7 +168,7 @@ mod tests {
assert!(rust.cxx_mod.content.is_none());
assert_eq!(rust.cxx_mod_contents.len(), 0);
assert_eq!(rust.namespace, "");
assert_eq!(rust.fragments.len(), 1);
assert_eq!(rust.fragments.len(), 2);
}

#[test]
Expand All @@ -163,7 +188,7 @@ mod tests {
assert!(rust.cxx_mod.content.is_none());
assert_eq!(rust.cxx_mod_contents.len(), 0);
assert_eq!(rust.namespace, "cxx_qt");
assert_eq!(rust.fragments.len(), 1);
assert_eq!(rust.fragments.len(), 2);
}

#[test]
Expand All @@ -183,6 +208,6 @@ mod tests {
assert!(rust.cxx_mod.content.is_none());
assert_eq!(rust.cxx_mod_contents.len(), 0);
assert_eq!(rust.namespace, "");
assert_eq!(rust.fragments.len(), 1);
assert_eq!(rust.fragments.len(), 2);
}
}
108 changes: 71 additions & 37 deletions crates/cxx-qt-gen/src/generator/rust/qobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
use crate::generator::structuring::StructuredQObject;
use crate::naming::Name;
use crate::{
generator::{
naming::{namespace::NamespaceName, qobject::QObjectNames},
Expand All @@ -14,8 +15,8 @@ use crate::{
},
naming::TypeNames,
};
use quote::quote;
use syn::{parse_quote, Attribute, Ident, Result};
use quote::{format_ident, quote};
use syn::{parse_quote, Attribute, Result};

impl GeneratedRustFragment {
// Might need to be refactored to use a StructuredQObject instead (confirm with Leon)
Expand All @@ -29,12 +30,7 @@ impl GeneratedRustFragment {
let namespace_idents = NamespaceName::from(qobject);

let mut generated = vec![
generate_qobject_definitions(
&qobject_names,
qobject.base_class.clone(),
type_names,
&qobject.cfgs,
)?,
generate_qobject_definitions(&qobject_names, &qobject.cfgs)?,
generate_rust_properties(
&qobject.properties,
&qobject_names,
Expand Down Expand Up @@ -67,6 +63,53 @@ impl GeneratedRustFragment {
)?);
}

// Generate casting impl
let base = structured_qobject
.declaration
.base_class
.as_ref()
.map(|name| type_names.lookup(name))
.transpose()?
.cloned()
.unwrap_or(Name::new(format_ident!("QObject")).with_module(parse_quote! {::cxx_qt}));

let base_unqualified = base.rust_unqualified();
let base_qualified = base.rust_qualified();

let struct_name = structured_qobject.declaration.name.rust_qualified();
let struct_name_unqualified = structured_qobject.declaration.name.rust_unqualified();
let (upcast_fn, upcast_fn_attrs, upcast_fn_qualified) = qobject_names
.cxx_qt_ffi_method("upcastPtr")
.into_cxx_parts();
let (downcast_fn, downcast_fn_attrs, downcast_fn_qualified) = qobject_names
.cxx_qt_ffi_method("downcastPtr")
.into_cxx_parts();

generated.push(GeneratedRustFragment {
cxx_mod_contents: vec![parse_quote! {
extern "C++" {
#[doc(hidden)]
#(#upcast_fn_attrs)*
unsafe fn #upcast_fn(thiz: *const #struct_name_unqualified) -> *const #base_unqualified;

#[doc(hidden)]
#(#downcast_fn_attrs)*
unsafe fn #downcast_fn(base: *const #base_unqualified) -> *const #struct_name_unqualified;
}
}],
cxx_qt_mod_contents: vec![parse_quote! {
impl ::cxx_qt::Upcast<#base_qualified> for #struct_name{
unsafe fn upcast_ptr(this: *const Self) -> *const #base_qualified {
#upcast_fn_qualified(this)
}

unsafe fn from_base_ptr(base: *const #base_qualified) -> *const Self {
#downcast_fn_qualified(base)
}
}
}],
});

generated.extend(vec![
constructor::generate(
&structured_qobject.constructors,
Expand All @@ -85,8 +128,6 @@ impl GeneratedRustFragment {
/// Generate the C++ and Rust CXX definitions for the QObject
fn generate_qobject_definitions(
qobject_idents: &QObjectNames,
base: Option<Ident>,
type_names: &TypeNames,
cfgs: &[Attribute],
) -> Result<GeneratedRustFragment> {
let cpp_class_name_rust = &qobject_idents.name.rust_unqualified();
Expand All @@ -106,29 +147,6 @@ fn generate_qobject_definitions(
}
};

let cpp_struct_qualified = &qobject_idents.name.rust_qualified();

let base_upcast = if let Some(base) = base {
let base_name = type_names.lookup(&base)?.rust_qualified();
vec![
parse_quote! {
#(#cfgs)*
impl cxx_qt::Upcast<#base_name> for #cpp_struct_qualified {}
},
// Until we can actually implement the Upcast trait properly, we just need to silence
// the warning that the base class is otherwise unused.
// This can be done with an unnamed import and the right attributes
parse_quote! {
#[allow(unused_imports)]
#[allow(dead_code)]
#(#cfgs)*
use #base_name as _;
},
]
} else {
vec![]
};

Ok(GeneratedRustFragment {
cxx_mod_contents: vec![
parse_quote! {
Expand Down Expand Up @@ -157,7 +175,7 @@ fn generate_qobject_definitions(
}
},
],
cxx_qt_mod_contents: base_upcast,
cxx_qt_mod_contents: vec![],
})
}

Expand Down Expand Up @@ -204,7 +222,7 @@ mod tests {
&parser.type_names,
)
.unwrap();
assert_eq!(rust.cxx_mod_contents.len(), 6);
assert_eq!(rust.cxx_mod_contents.len(), 7);
assert_tokens_eq(
&rust.cxx_mod_contents[0],
quote! {
Expand Down Expand Up @@ -239,6 +257,22 @@ mod tests {
);
assert_tokens_eq(
&rust.cxx_mod_contents[3],
quote! {
extern "C++" {
#[doc(hidden)]
#[cxx_name = "upcastPtr"]
#[namespace = "rust::cxxqt1"]
unsafe fn cxx_qt_ffi_MyObject_upcastPtr(thiz: *const MyObject) -> *const QObject;

#[doc(hidden)]
#[cxx_name = "downcastPtr"]
#[namespace = "rust::cxxqt1"]
unsafe fn cxx_qt_ffi_MyObject_downcastPtr(base: *const QObject) -> *const MyObject;
}
},
);
assert_tokens_eq(
&rust.cxx_mod_contents[4],
quote! {
extern "Rust" {
#[cxx_name = "createRs"]
Expand All @@ -248,7 +282,7 @@ mod tests {
},
);
assert_tokens_eq(
&rust.cxx_mod_contents[4],
&rust.cxx_mod_contents[5],
quote! {
unsafe extern "C++" {
#[doc(hidden)]
Expand All @@ -259,7 +293,7 @@ mod tests {
},
);
assert_tokens_eq(
&rust.cxx_mod_contents[5],
&rust.cxx_mod_contents[6],
quote! {
unsafe extern "C++" {
#[doc(hidden)]
Expand Down
1 change: 1 addition & 0 deletions crates/cxx-qt-gen/src/writer/cpp/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ mod tests {
let expected = indoc! {r#"
#pragma once

#include <cxx-qt/casting.h>
#include <cxx-qt/type.h>

class MyObject;
Expand Down
1 change: 1 addition & 0 deletions crates/cxx-qt-gen/test_outputs/cfgs.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <cstdint>
#include <cxx-qt/casting.h>
#include <cxx-qt/signalhandler.h>
#include <cxx-qt/type.h>

Expand Down
Loading
Loading