Skip to content

Commit b0a9700

Browse files
committed
cxx-qt-gen: allow for optional #[qobject] macro
This allows for using CXX-Qt helpers with normal C++ classes, eg #[inherit] and #[cxx_override] etc. Closes #824
1 parent 5f92133 commit b0a9700

File tree

8 files changed

+186
-34
lines changed

8 files changed

+186
-34
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3232
- `cxx-qt-gen` now does not generate code requiring `cxx-qt-lib`, this allows for `cxx-qt-lib` to be optional
3333
- `cxx-qt-lib` headers must be given to `cxx-qt-build` with `.with_opts(cxx_qt_lib_headers::build_opts())`
3434
- File name is used for CXX bridges rather than module name to match upstream
35+
- `#[qobject]` attribute is now optional on types in `extern "RustQt"`
3536

3637
### Fixed
3738

crates/cxx-qt-gen/src/generator/cpp/constructor.rs

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ fn default_constructor(
1717
base_class: String,
1818
initializers: String,
1919
) -> GeneratedCppQObjectBlocks {
20-
GeneratedCppQObjectBlocks {
21-
methods: vec![CppFragment::Pair {
20+
let constructor = if qobject.has_qobject_macro {
21+
CppFragment::Pair {
2222
header: format!(
2323
"explicit {class_name}(QObject* parent = nullptr);",
2424
class_name = qobject.ident
@@ -34,7 +34,33 @@ fn default_constructor(
3434
namespace_internals = qobject.namespace_internals,
3535
rust_obj = qobject.rust_ident,
3636
),
37-
}],
37+
}
38+
} else {
39+
CppFragment::Pair {
40+
header: format!("explicit {class_name}();", class_name = qobject.ident),
41+
source: formatdoc!(
42+
r#"
43+
{class_name}::{class_name}()
44+
{base_class_line}
45+
, ::rust::cxxqt1::CxxQtType<{rust_obj}>(::{namespace_internals}::createRs()){initializers}
46+
{{ }}
47+
"#,
48+
base_class_line = if base_class.is_empty() {
49+
unreachable!(
50+
"Cannot have an empty #[base] attribute with no #[qobject] attribute"
51+
);
52+
} else {
53+
format!(": {base_class}()")
54+
},
55+
class_name = qobject.ident,
56+
namespace_internals = qobject.namespace_internals,
57+
rust_obj = qobject.rust_ident,
58+
),
59+
}
60+
};
61+
62+
GeneratedCppQObjectBlocks {
63+
methods: vec![constructor],
3864
..Default::default()
3965
}
4066
}
@@ -143,6 +169,7 @@ mod tests {
143169
namespace: "".to_string(),
144170
namespace_internals: "rust".to_string(),
145171
blocks: GeneratedCppQObjectBlocks::default(),
172+
has_qobject_macro: true,
146173
}
147174
}
148175

@@ -222,6 +249,37 @@ mod tests {
222249
);
223250
}
224251

252+
#[test]
253+
fn default_constructor_no_qobject_macro() {
254+
let mut qobject = qobject_for_testing();
255+
qobject.has_qobject_macro = false;
256+
let blocks = generate(
257+
&qobject,
258+
&[],
259+
"BaseClass".to_owned(),
260+
&[],
261+
&TypeNames::default(),
262+
)
263+
.unwrap();
264+
265+
assert_empty_blocks(&blocks);
266+
assert!(blocks.private_methods.is_empty());
267+
assert_eq!(
268+
blocks.methods,
269+
vec![CppFragment::Pair {
270+
header: "explicit MyObject();".to_string(),
271+
source: formatdoc!(
272+
"
273+
MyObject::MyObject()
274+
: BaseClass()
275+
, ::rust::cxxqt1::CxxQtType<MyObjectRust>(::rust::createRs())
276+
{{ }}
277+
"
278+
),
279+
}]
280+
);
281+
}
282+
225283
#[test]
226284
fn constructor_without_base_arguments() {
227285
let blocks = generate(

crates/cxx-qt-gen/src/generator/cpp/inherit.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ pub fn generate(
2222

2323
for method in inherited_methods {
2424
let return_type = syn_type_to_cpp_return_type(&method.method.sig.output, type_names)?;
25+
// Note that no qobject macro with no base class is an error
26+
//
27+
// So a default of QObject is fine here
2528
let base_class = base_class.as_deref().unwrap_or("QObject");
2629

2730
result.methods.push(CppFragment::Header(formatdoc! {

crates/cxx-qt-gen/src/generator/cpp/qobject.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ pub struct GeneratedCppQObject {
9292
pub namespace_internals: String,
9393
/// The blocks of the QObject
9494
pub blocks: GeneratedCppQObjectBlocks,
95+
/// Whether this type has a #[qobject] / Q_OBJECT macro
96+
pub has_qobject_macro: bool,
9597
}
9698

9799
impl GeneratedCppQObject {
@@ -106,6 +108,7 @@ impl GeneratedCppQObject {
106108
namespace: qobject.namespace.clone(),
107109
namespace_internals: namespace_idents.internal,
108110
blocks: GeneratedCppQObjectBlocks::from(qobject),
111+
has_qobject_macro: qobject.has_qobject_macro,
109112
};
110113

111114
// Ensure that we include MaybeLockGuard<T> that is used in multiple places
@@ -115,10 +118,16 @@ impl GeneratedCppQObject {
115118
.insert("#include <cxx-qt/maybelockguard.h>".to_owned());
116119

117120
// Build the base class
118-
let base_class = qobject
119-
.base_class
120-
.clone()
121-
.unwrap_or_else(|| "QObject".to_string());
121+
let base_class = qobject.base_class.clone().unwrap_or_else(|| {
122+
// If there is a QObject macro then assume the base class is QObject
123+
if qobject.has_qobject_macro {
124+
"QObject".to_string()
125+
} else {
126+
unreachable!(
127+
"Cannot have an empty #[base] attribute with no #[qobject] attribute"
128+
);
129+
}
130+
});
122131
generated.blocks.base_classes.push(base_class.clone());
123132

124133
// Add the CxxQtType rust and rust_mut methods

crates/cxx-qt-gen/src/parser/cxxqtdata.rs

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -80,27 +80,44 @@ impl ParsedCxxQtData {
8080
syn::parse2(tokens.clone())?;
8181

8282
// Check this type is tagged with a #[qobject]
83-
if attribute_take_path(&mut foreign_alias.attrs, &["qobject"])
84-
.is_some()
83+
let has_qobject_macro =
84+
attribute_take_path(&mut foreign_alias.attrs, &["qobject"])
85+
.is_some();
86+
87+
// Load the QObject
88+
let mut qobject = ParsedQObject::try_from(&foreign_alias)?;
89+
qobject.has_qobject_macro = has_qobject_macro;
90+
91+
// Ensure that the base class attribute is not empty, as this is not valid in both cases
92+
// - when there is a qobject macro it is not valid
93+
// - when there is not a qobject macro it is not valid
94+
if qobject
95+
.base_class
96+
.as_ref()
97+
.is_some_and(|base| base.is_empty())
8598
{
86-
// Load the QObject
87-
let mut qobject = ParsedQObject::try_from(&foreign_alias)?;
88-
89-
// Inject the bridge namespace if the qobject one is empty
90-
if qobject.namespace.is_empty() && namespace.is_some() {
91-
qobject.namespace = namespace.clone().unwrap();
92-
}
93-
94-
// Note that we assume a compiler error will occur later
95-
// if you had two structs with the same name
96-
self.qobjects
97-
.insert(foreign_alias.ident_left.clone(), qobject);
98-
} else {
9999
return Err(Error::new(
100100
foreign_item.span(),
101-
"type A = super::B must be tagged with #[qobject]",
101+
"The #[base] attribute cannot be empty",
102102
));
103103
}
104+
105+
// Ensure that if there is no qobject macro that a base class is specificed
106+
//
107+
// Note this assumes the check above
108+
if !qobject.has_qobject_macro && qobject.base_class.is_none() {
109+
return Err(Error::new(foreign_item.span(), "A type without a #[qobject] attribute must specify a #[base] attribute"));
110+
}
111+
112+
// Inject the bridge namespace if the qobject one is empty
113+
if qobject.namespace.is_empty() && namespace.is_some() {
114+
qobject.namespace = namespace.clone().unwrap();
115+
}
116+
117+
// Note that we assume a compiler error will occur later
118+
// if you had two structs with the same name
119+
self.qobjects
120+
.insert(foreign_alias.ident_left.clone(), qobject);
104121
}
105122
// Const Macro, Type are unsupported in extern "RustQt" for now
106123
_others => {
@@ -316,6 +333,13 @@ mod tests {
316333
assert!(result.is_ok());
317334
assert_eq!(cxx_qt_data.qobjects.len(), 1);
318335
assert!(cxx_qt_data.qobjects.contains_key(&qobject_ident()));
336+
assert!(
337+
cxx_qt_data
338+
.qobjects
339+
.get(&qobject_ident())
340+
.unwrap()
341+
.has_qobject_macro
342+
);
319343
}
320344

321345
#[test]
@@ -380,7 +404,7 @@ mod tests {
380404
}
381405

382406
#[test]
383-
fn test_find_qobjects_no_qobject() {
407+
fn test_find_qobjects_no_qobject_no_base() {
384408
let mut cxx_qt_data = ParsedCxxQtData::new(format_ident!("ffi"), None);
385409

386410
let module: ItemMod = parse_quote! {
@@ -395,6 +419,39 @@ mod tests {
395419
assert!(result.is_err());
396420
}
397421

422+
#[test]
423+
fn test_find_qobjects_no_qobject_with_base() {
424+
let mut cxx_qt_data = ParsedCxxQtData::new(format_ident!("ffi"), None);
425+
426+
let module: ItemMod = parse_quote! {
427+
mod module {
428+
extern "RustQt" {
429+
#[base = "OtherBase"]
430+
type Other = super::OtherRust;
431+
#[base = "MyObjectBase"]
432+
type MyObject = super::MyObjectRust;
433+
}
434+
}
435+
};
436+
let result = cxx_qt_data.find_qobject_types(&module.content.unwrap().1);
437+
assert!(result.is_ok());
438+
assert_eq!(cxx_qt_data.qobjects.len(), 2);
439+
assert!(
440+
!cxx_qt_data
441+
.qobjects
442+
.get(&format_ident!("Other"))
443+
.unwrap()
444+
.has_qobject_macro
445+
);
446+
assert!(
447+
!cxx_qt_data
448+
.qobjects
449+
.get(&format_ident!("MyObject"))
450+
.unwrap()
451+
.has_qobject_macro
452+
);
453+
}
454+
398455
#[test]
399456
fn test_find_and_merge_cxx_qt_item_struct_qobject_passthrough() {
400457
let mut cxx_qt_data = create_parsed_cxx_qt_data();

crates/cxx-qt-gen/src/parser/qobject.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ pub struct ParsedQObject {
5757
pub locking: bool,
5858
/// Whether threading has been enabled for this QObject
5959
pub threading: bool,
60+
/// Whether this type has a #[qobject] / Q_OBJECT macro
61+
pub has_qobject_macro: bool,
6062
}
6163

6264
impl TryFrom<&ForeignTypeIdentAlias> for ParsedQObject {
@@ -97,6 +99,7 @@ impl TryFrom<&ForeignTypeIdentAlias> for ParsedQObject {
9799
qml_metadata,
98100
locking: true,
99101
threading: false,
102+
has_qobject_macro: false,
100103
})
101104
}
102105
}

crates/cxx-qt-gen/src/writer/cpp/header.rs

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,23 @@ fn forward_declare(generated: &GeneratedCppBlocks) -> Vec<String> {
7676
/// For a given GeneratedCppBlocks write the classes
7777
fn qobjects_header(generated: &GeneratedCppBlocks) -> Vec<String> {
7878
generated.qobjects.iter().map(|qobject| {
79+
let ident = &qobject.ident;
80+
let qobject_macro = if qobject.has_qobject_macro {
81+
"Q_OBJECT"
82+
} else {
83+
""
84+
};
85+
let qobject_assert = if qobject.has_qobject_macro {
86+
format!("static_assert(::std::is_base_of<QObject, {ident}>::value, \"{ident} must inherit from QObject\");")
87+
} else {
88+
"".to_owned()
89+
};
7990
let class_definition = namespaced(
8091
&qobject.namespace,
8192
&formatdoc! { r#"
8293
class {ident} : {base_classes}
8394
{{
84-
Q_OBJECT
95+
{qobject_macro}
8596
public:
8697
{metaobjects}
8798
@@ -91,8 +102,8 @@ fn qobjects_header(generated: &GeneratedCppBlocks) -> Vec<String> {
91102
{private_methods}
92103
}};
93104
94-
static_assert(::std::is_base_of<QObject, {ident}>::value, "{ident} must inherit from QObject");"#,
95-
ident = qobject.ident,
105+
{qobject_assert}"#,
106+
// Note that there is always a base class as we always have CxxQtType
96107
base_classes = qobject.blocks.base_classes.iter().map(|base| format!("public {}", base)).collect::<Vec<String>>().join(", "),
97108
metaobjects = qobject.blocks.metaobjects.join("\n "),
98109
public_methods = create_block("public", &qobject.blocks.methods.iter().filter_map(pair_as_header).collect::<Vec<String>>()),
@@ -107,17 +118,24 @@ fn qobjects_header(generated: &GeneratedCppBlocks) -> Vec<String> {
107118
.collect::<Vec<String>>()
108119
.join("\n");
109120

121+
let declare_metatype = if qobject.has_qobject_macro {
122+
let ty = if qobject.namespace.is_empty() {
123+
qobject.ident.clone()
124+
} else {
125+
format!("{namespace}::{ident}", namespace = qobject.namespace, ident = qobject.ident)
126+
};
127+
format!("Q_DECLARE_METATYPE({ty}*)")
128+
} else {
129+
"".to_owned()
130+
};
131+
110132
formatdoc! {r#"
111133
{fragments}
112134
{class_definition}
113135
114-
Q_DECLARE_METATYPE({metatype}*)
115-
"#,
116-
metatype = if qobject.namespace.is_empty() {
117-
qobject.ident.clone()
118-
} else {
119-
format!("{namespace}::{ident}", namespace = qobject.namespace, ident = qobject.ident)
120-
},}
136+
{declare_metatype}
137+
"#
138+
}
121139
}).collect::<Vec<String>>()
122140
}
123141

crates/cxx-qt-gen/src/writer/cpp/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ mod tests {
6262
rust_ident: "MyObjectRust".to_owned(),
6363
namespace: "cxx_qt::my_object".to_owned(),
6464
namespace_internals: "cxx_qt::my_object::cxx_qt_my_object".to_owned(),
65+
has_qobject_macro: true,
6566
blocks: GeneratedCppQObjectBlocks {
6667
base_classes: vec!["QStringListModel".to_owned()],
6768
includes: {
@@ -185,6 +186,7 @@ mod tests {
185186
rust_ident: "FirstObjectRust".to_owned(),
186187
namespace: "cxx_qt".to_owned(),
187188
namespace_internals: "cxx_qt::cxx_qt_first_object".to_owned(),
189+
has_qobject_macro: true,
188190
blocks: GeneratedCppQObjectBlocks {
189191
base_classes: vec!["QStringListModel".to_owned()],
190192
includes: {
@@ -228,6 +230,7 @@ mod tests {
228230
rust_ident: "SecondObjectRust".to_owned(),
229231
namespace: "cxx_qt".to_owned(),
230232
namespace_internals: "cxx_qt::cxx_qt_second_object".to_owned(),
233+
has_qobject_macro: true,
231234
blocks: GeneratedCppQObjectBlocks {
232235
base_classes: vec!["QStringListModel".to_owned()],
233236
includes: {

0 commit comments

Comments
 (0)