Skip to content

Commit

Permalink
register QML elements at build time
Browse files Browse the repository at this point in the history
by specifying
[cxxqt::qobject(qml_uri = "foo.bar", qml_verison = "1.0")]

Fixes #241
  • Loading branch information
vimpostor authored and Be-ing committed Feb 7, 2023
1 parent 202889f commit 467f995
Show file tree
Hide file tree
Showing 11 changed files with 304 additions and 66 deletions.
62 changes: 51 additions & 11 deletions crates/cxx-qt-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::{

use cxx_qt_gen::{
parse_qt_file, write_cpp, write_rust, CppFragment, CxxQtItem, GeneratedCppBlocks,
GeneratedRustBlocks, Parser,
GeneratedRustBlocks, Parser, QmlElementMetadata,
};

// TODO: we need to eventually support having multiple modules defined in a single file. This
Expand All @@ -30,13 +30,14 @@ use cxx_qt_gen::{
struct GeneratedCppFilePaths {
plain_cpp: PathBuf,
qobject: Option<PathBuf>,
qobject_header: Option<PathBuf>,
qobject_header: Option<QObjectHeader>,
}

struct GeneratedCpp {
cxx_qt: Option<CppFragment>,
cxx: cxx_gen::GeneratedCode,
file_ident: String,
qml_metadata: Option<QmlElementMetadata>,
}

impl GeneratedCpp {
Expand All @@ -46,6 +47,7 @@ impl GeneratedCpp {
let file = parse_qt_file(rust_file_path).unwrap();

let mut cxx_qt = None;
let mut qml_metadata = None;
// TODO: later change how the resultant filename is chosen, can we match the input file like
// CXX does?
//
Expand Down Expand Up @@ -92,6 +94,7 @@ impl GeneratedCpp {
let generated_rust = GeneratedRustBlocks::from(&parser).unwrap();
let rust_tokens = write_rust(&generated_rust);
file_ident = parser.cxx_file_stem.clone();
qml_metadata = parser.cxx_qt_data.qml_metadata;

// We need to do this and can't rely on the macro, as we need to generate the
// CXX bridge Rust code that is then fed into the cxx_gen generation.
Expand All @@ -111,6 +114,7 @@ impl GeneratedCpp {
cxx_qt,
cxx,
file_ident,
qml_metadata,
}
}

Expand Down Expand Up @@ -148,7 +152,10 @@ impl GeneratedCpp {
header
.write_all(header_generated.as_bytes())
.expect("Could not write cxx-qt header file");
cpp_file_paths.qobject_header = Some(header_path);
cpp_file_paths.qobject_header = Some(QObjectHeader {
path: header_path,
qml_metadata: self.qml_metadata.clone(),
});

let cpp_path = PathBuf::from(format!(
"{}/{}.cxxqt.cpp",
Expand Down Expand Up @@ -210,6 +217,11 @@ fn generate_cxxqt_cpp_files(
generated_file_paths
}

struct QObjectHeader {
path: PathBuf,
qml_metadata: Option<QmlElementMetadata>,
}

/// Run cxx-qt's C++ code generator on Rust modules marked with the [cxx_qt::bridge] macro, compile
/// the code, and link to Qt. This is the complement of the [cxx_qt::bridge] macro, which the Rust
/// compiler uses to generate the corresponding Rust code. No dependencies besides Qt, a C++17 compiler,
Expand Down Expand Up @@ -245,7 +257,7 @@ fn generate_cxxqt_cpp_files(
#[derive(Default)]
pub struct CxxQtBuilder {
rust_sources: Vec<PathBuf>,
qobject_headers: Vec<PathBuf>,
qobject_headers: Vec<QObjectHeader>,
qrc_files: Vec<PathBuf>,
qt_modules: HashSet<String>,
cc_builder: cc::Build,
Expand Down Expand Up @@ -317,7 +329,10 @@ impl CxxQtBuilder {
/// This allows building QObject C++ subclasses besides the ones autogenerated by cxx-qt.
pub fn qobject_header(mut self, path: impl AsRef<Path>) -> Self {
let path = path.as_ref();
self.qobject_headers.push(path.to_path_buf());
self.qobject_headers.push(QObjectHeader {
path: path.to_path_buf(),
qml_metadata: None,
});
println!("cargo:rerun-if-changed={}", path.display());
self
}
Expand Down Expand Up @@ -370,17 +385,20 @@ impl CxxQtBuilder {
.expect("Could not find Qt installation");
qtbuild.cargo_link_libraries();
for include_dir in qtbuild.include_paths() {
self.cc_builder.include(include_dir);
self.cc_builder.include(&include_dir);
}

let out_dir = env::var("OUT_DIR").unwrap();

// The include directory needs to be namespaced by crate name when exporting for a C++ build system,
// but for using cargo build without a C++ build system, OUT_DIR is already namespaced by crate name.
let header_root = match env::var("CXXQT_EXPORT_DIR") {
Ok(export_dir) => format!("{export_dir}/{}", env::var("CARGO_PKG_NAME").unwrap()),
Err(_) => env::var("OUT_DIR").unwrap(),
Err(_) => out_dir,
};
self.cc_builder.include(&header_root);
let generated_header_dir = format!("{header_root}/cxx-qt-gen");
self.cc_builder.include(&generated_header_dir);

cxx_qt_lib_headers::write_headers(format!("{header_root}/cxx-qt-lib"));

Expand All @@ -404,17 +422,39 @@ impl CxxQtBuilder {
}
}

// Static QML plugin and Qt resource initialization need to be linked with +whole-archive
// because they use static variables which need to be initialized before main
// (regardless of whether main is in Rust or C++). Normally linkers only copy symbols referenced
// from within main when static linking, which would result in discarding those static variables.
// Use a separate cc::Build for little amount of code that needs to be linked with +whole-archive
// to avoid bloating the binary.
let mut cc_builder_whole_archive = self.cc_builder.clone();
cc_builder_whole_archive.link_lib_modifier("+whole-archive");

// Run moc on C++ headers with Q_OBJECT macro
for qobject_header in self.qobject_headers {
self.cc_builder.file(qtbuild.moc(&qobject_header));
let moc_products = qtbuild.moc(&qobject_header.path);
self.cc_builder.file(moc_products.cpp);
if let Some(qml_metadata) = qobject_header.qml_metadata {
self.cc_builder.define("QT_STATICPLUGIN", None);
let qml_type_registration_files = qtbuild.register_qml_types(
moc_products.metatypes_json,
qml_metadata.version_major,
qml_metadata.version_minor,
&qml_metadata.uri,
);
self.cc_builder
.file(qml_type_registration_files.qmltyperegistrar);
self.cc_builder.file(qml_type_registration_files.plugin);
cc_builder_whole_archive.file(qml_type_registration_files.plugin_init);
}
}

// Generate code from .qrc files, but do not compile it. Instead, the user needs to #include them
// in a .cpp file. Otherwise, MSVC won't link if the generated C++ is built separately.
for qrc_file in self.qrc_files {
qtbuild.qrc(&qrc_file);
cc_builder_whole_archive.file(qtbuild.qrc(&qrc_file));
}

cc_builder_whole_archive.compile("qt-static-initializers");
self.cc_builder.compile("cxx-qt-gen");
}
}
24 changes: 23 additions & 1 deletion crates/cxx-qt-gen/src/generator/cpp/qobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,28 @@ impl GeneratedCppQObjectBlocks {
self.metaobjects.append(&mut other.metaobjects);
self.methods.append(&mut other.methods);
}

pub fn from(qobject: &ParsedQObject) -> GeneratedCppQObjectBlocks {
let mut qml_specifiers = Vec::new();
if let Some(qml_metadata) = &qobject.qml_metadata {
qml_specifiers.push(format!(
"Q_CLASSINFO(\"QML.Element\", \"{}\")",
qml_metadata.name
));
// TODO untested
if qml_metadata.uncreatable {
qml_specifiers.push("Q_CLASSINFO(\"QML.Creatable\", \"false\")".to_owned());
}
// TODO untested
if qml_metadata.singleton {
qml_specifiers.push("QML_SINGLETON".to_owned());
}
}
GeneratedCppQObjectBlocks {
metaobjects: qml_specifiers,
..Default::default()
}
}
}

#[derive(Default)]
Expand Down Expand Up @@ -61,7 +83,7 @@ impl GeneratedCppQObject {
.base_class
.clone()
.unwrap_or_else(|| "QObject".to_string()),
..Default::default()
blocks: GeneratedCppQObjectBlocks::from(qobject),
};

// Generate methods for the properties, invokables, signals
Expand Down
2 changes: 1 addition & 1 deletion crates/cxx-qt-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub use generator::{
cpp::{fragment::CppFragment, GeneratedCppBlocks},
rust::GeneratedRustBlocks,
};
pub use parser::Parser;
pub use parser::{qobject::QmlElementMetadata, Parser};
pub use syntax::{parse_qt_file, CxxQtItem};
pub use writer::{cpp::write_cpp, rust::write_rust};

Expand Down
52 changes: 50 additions & 2 deletions crates/cxx-qt-gen/src/parser/cxxqtdata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::parser::{qobject::ParsedQObject, signals::ParsedSignalsEnum};
use crate::parser::{
qobject::{ParsedQObject, QmlElementMetadata},
signals::ParsedSignalsEnum,
};
use crate::syntax::attribute::{
attribute_tokens_to_map, attribute_tokens_to_value, AttributeDefault,
};
Expand Down Expand Up @@ -61,6 +64,8 @@ pub struct ParsedCxxQtData {
pub namespace: String,
/// Any `use` statements end up in the CXX-Qt generated module
pub uses: Vec<Item>,
/// Metadata for registering QML element
pub qml_metadata: Option<QmlElementMetadata>,
}

impl ParsedCxxQtData {
Expand All @@ -71,8 +76,51 @@ impl ParsedCxxQtData {
if let Some(index) =
attribute_find_path(&qobject_struct.attrs, &["cxx_qt", "qobject"])
{
let attrs_map = attribute_tokens_to_map::<Ident, LitStr>(
&qobject_struct.attrs[index],
AttributeDefault::Some(|span| LitStr::new("", span)),
)?;

// Parse QML metadata
if let (Some(qml_uri), Some(qml_version)) = (
attrs_map.get(&quote::format_ident!("qml_uri")),
attrs_map.get(&quote::format_ident!("qml_version")),
) {
let qml_version = qml_version.value();
let version_parts: Vec<_> = qml_version.split('.').collect();
let version_major = version_parts[0]
.parse()
.expect("Could not parse major version from qml_version");
let version_minor = version_parts[1].parse().unwrap_or(0);

let name = match attrs_map.get(&quote::format_ident!("qml_name")) {
Some(qml_name) => qml_name.value(),
None => qobject_struct.ident.to_string(),
};

let uncreatable = attrs_map
.get(&quote::format_ident!("qml_uncreatable"))
.is_some();
let singleton = attrs_map
.get(&quote::format_ident!("qml_singleton"))
.is_some();

self.qml_metadata = Some(QmlElementMetadata {
uri: qml_uri.value(),
name,
version_major,
version_minor,
uncreatable,
singleton,
});
}

// Load the QObject
let mut qobject = ParsedQObject::from_struct(qobject_struct, index)?;
let mut qobject = ParsedQObject::from_struct(
qobject_struct,
index,
self.qml_metadata.clone(),
)?;

// Parse any namespaces
let attr_map = attribute_tokens_to_map::<Ident, LitStr>(
Expand Down
36 changes: 28 additions & 8 deletions crates/cxx-qt-gen/src/parser/qobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ use syn::{
Result,
};

/// Metadata for registering QML element
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct QmlElementMetadata {
pub uri: String,
pub name: String,
pub version_major: usize,
pub version_minor: usize,
pub uncreatable: bool,
pub singleton: bool,
}

/// A representation of a QObject within a CXX-Qt [syn::ItemMod]
///
/// This has initial splitting of [syn::Item]'s into relevant blocks, other phases will
Expand Down Expand Up @@ -45,14 +56,20 @@ pub struct ParsedQObject {
pub properties: Vec<ParsedQProperty>,
/// List of Rust fields on the struct that need getters and setters generated
pub fields: Vec<ParsedRustField>,
/// List of specifiers to register with in QML
pub qml_metadata: Option<QmlElementMetadata>,
/// Items that we don't need to generate anything for CXX or C++
/// eg impls on the Rust object or Default implementations
pub others: Vec<Item>,
}

impl ParsedQObject {
/// Parse a [syn::ItemStruct] into a [ParsedQObject] with the index of the cxx_qt::qobject specified
pub fn from_struct(qobject_struct: &ItemStruct, attr_index: usize) -> Result<Self> {
pub fn from_struct(
qobject_struct: &ItemStruct,
attr_index: usize,
qml_metadata: Option<QmlElementMetadata>,
) -> Result<Self> {
// Find if there is any base class
let base_class = attribute_tokens_to_map::<Ident, LitStr>(
&qobject_struct.attrs[attr_index],
Expand All @@ -79,6 +96,7 @@ impl ParsedQObject {
methods: vec![],
properties,
fields,
qml_metadata,
others: vec![],
})
}
Expand Down Expand Up @@ -164,7 +182,7 @@ pub mod tests {
#[cxx_qt::qobject]
struct MyObject;
});
ParsedQObject::from_struct(&qobject_struct, 0).unwrap()
ParsedQObject::from_struct(&qobject_struct, 0, None).unwrap()
}

#[test]
Expand All @@ -174,7 +192,7 @@ pub mod tests {
struct MyObject;
});

let qobject = ParsedQObject::from_struct(&qobject_struct, 0).unwrap();
let qobject = ParsedQObject::from_struct(&qobject_struct, 0, None).unwrap();
assert!(qobject.base_class.is_none());
}

Expand All @@ -185,7 +203,7 @@ pub mod tests {
struct MyObject;
});

let qobject = ParsedQObject::from_struct(&qobject_struct, 0).unwrap();
let qobject = ParsedQObject::from_struct(&qobject_struct, 0, None).unwrap();
assert_eq!(qobject.base_class.as_ref().unwrap(), "QStringListModel");
}

Expand All @@ -204,7 +222,7 @@ pub mod tests {
}
});

let qobject = ParsedQObject::from_struct(&qobject_struct, 0).unwrap();
let qobject = ParsedQObject::from_struct(&qobject_struct, 0, None).unwrap();
assert_eq!(qobject.properties.len(), 2);
assert_eq!(qobject.qobject_struct.fields.len(), 3);
}
Expand All @@ -218,7 +236,7 @@ pub mod tests {
}
});

let qobject = ParsedQObject::from_struct(&qobject_struct, 0).unwrap();
let qobject = ParsedQObject::from_struct(&qobject_struct, 0, None).unwrap();
assert_eq!(qobject.properties.len(), 0);
assert_eq!(qobject.qobject_struct.fields.len(), 1);
}
Expand Down Expand Up @@ -303,7 +321,9 @@ pub mod tests {
field: f64,
}
});
let properties = ParsedQObject::from_struct(&item, 0).unwrap().properties;
let properties = ParsedQObject::from_struct(&item, 0, None)
.unwrap()
.properties;
assert_eq!(properties.len(), 3);
assert_eq!(properties[0].ident, "f64_property");
assert_eq!(properties[0].ty, f64_type());
Expand All @@ -327,6 +347,6 @@ pub mod tests {
#[cxx_qt::qobject]
struct T(f64);
});
assert!(ParsedQObject::from_struct(&item, 0).is_err());
assert!(ParsedQObject::from_struct(&item, 0, None).is_err());
}
}
Loading

0 comments on commit 467f995

Please sign in to comment.