Skip to content

Commit

Permalink
move QML element registration to build.rs instead of bridge
Browse files Browse the repository at this point in the history
This avoids the need to repeatedly specify the URI and version
of the QML module in the bridges. QObjects that are registered as
QML elements are now marked by
[cxx_qt::qobject(qml_element = "OptionalName")]

This is a precursor to generating qmldir files which are required
for qmlcachegen and qmltc
(KDAB#242)
  • Loading branch information
Be-ing committed Jul 25, 2023
1 parent e3013b1 commit 273a4a5
Show file tree
Hide file tree
Showing 22 changed files with 165 additions and 223 deletions.
167 changes: 94 additions & 73 deletions crates/cxx-qt-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use diagnostics::{Diagnostic, GeneratedError};
use convert_case::{Case, Casing};
use quote::ToTokens;
use std::{
collections::{HashMap, HashSet},
collections::HashSet,
env,
fs::File,
io::Write,
Expand All @@ -26,7 +26,7 @@ use std::{

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

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

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

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

/// Metadata for registering a QML module
struct QmlModule {
/// The URI of the QML module
pub uri: String,
/// The minor version of the QML module
pub version_minor: usize,
/// The major version of the QML module
pub version_major: usize,
/// The .rs files with #[qml_element] attribute(s)
pub rust_files: Vec<PathBuf>,
}

impl GeneratedCpp {
Expand All @@ -67,7 +73,6 @@ impl GeneratedCpp {
.map_err(to_diagnostic)?;

let mut cxx_qt = None;
let mut qml_metadata = Vec::new();
// TODO: later change how the resultant filename is chosen, can we match the input file like
// CXX does?
//
Expand Down Expand Up @@ -120,11 +125,6 @@ impl GeneratedCpp {
.map_err(to_diagnostic)?;
let rust_tokens = write_rust(&generated_rust);
file_ident = parser.cxx_file_stem.clone();
for (_, qobject) in parser.cxx_qt_data.qobjects {
if let Some(q) = qobject.qml_metadata {
qml_metadata.push(q);
}
}

// 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 @@ -145,7 +145,6 @@ impl GeneratedCpp {
cxx_qt,
cxx,
file_ident,
qml_metadata,
})
}

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

let cpp_path = PathBuf::from(format!(
"{}/{}.cxxqt.cpp",
Expand Down Expand Up @@ -232,7 +228,7 @@ impl GeneratedCpp {
fn generate_cxxqt_cpp_files(
rs_source: &[PathBuf],
header_dir: impl AsRef<Path>,
) -> Result<Vec<GeneratedCppFilePaths>, Diagnostic> {
) -> Vec<GeneratedCppFilePaths> {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();

let mut generated_file_paths: Vec<GeneratedCppFilePaths> = Vec::with_capacity(rs_source.len());
Expand All @@ -241,11 +237,17 @@ fn generate_cxxqt_cpp_files(
let path = format!("{manifest_dir}/{}", rs_path.display());
println!("cargo:rerun-if-changed={path}");

let generated_code = GeneratedCpp::new(&path)?;
let generated_code = match GeneratedCpp::new(&path) {
Ok(v) => v,
Err(diagnostic) => {
diagnostic.report();
std::process::exit(1);
}
};
generated_file_paths.push(generated_code.write_to_directories(cpp_directory, &header_dir));
}

Ok(generated_file_paths)
generated_file_paths
}

/// Run cxx-qt's C++ code generator on Rust modules marked with the `cxx_qt::bridge` macro, compile
Expand Down Expand Up @@ -283,9 +285,10 @@ fn generate_cxxqt_cpp_files(
#[derive(Default)]
pub struct CxxQtBuilder {
rust_sources: Vec<PathBuf>,
qobject_headers: Vec<QObjectHeader>,
qobject_headers: Vec<PathBuf>,
qrc_files: Vec<PathBuf>,
qt_modules: HashSet<String>,
qml_modules: Vec<QmlModule>,
cc_builder: cc::Build,
}

Expand All @@ -303,16 +306,22 @@ impl CxxQtBuilder {
qobject_headers: vec![],
qrc_files: vec![],
qt_modules,
qml_modules: vec![],
cc_builder: cc::Build::new(),
}
}

/// Specify rust file paths to parse through the cxx-qt marco
/// Relative paths are treated as relative to the path of your crate's Cargo.toml file
pub fn file(mut self, rust_source: impl AsRef<Path>) -> Self {
let rust_source = rust_source.as_ref();
self.rust_sources.push(rust_source.to_path_buf());
let rust_source = rust_source.as_ref().to_path_buf();
for qml_module in &self.qml_modules {
if qml_module.rust_files.contains(&rust_source) {
panic!("CXX-Qt bridge Rust file {} specified in QML module {} (version {}.{}), but also specified via CxxQtBridge::file. Bridge files must be specified via CxxQtBuilder::file or CxxQtBuilder::qml_module, but not both.", rust_source.display(), qml_module.uri, qml_module.version_major, qml_module.version_minor);
}
}
println!("cargo:rerun-if-changed={}", rust_source.display());
self.rust_sources.push(rust_source);
self
}

Expand Down Expand Up @@ -351,14 +360,36 @@ impl CxxQtBuilder {
self
}

/// Register a QML module at build time
pub fn qml_module(
mut self,
uri: &str,
version_major: usize,
version_minor: usize,
rust_files: &[impl AsRef<Path>],
) -> Self {
for path in rust_files {
if self.rust_sources.contains(&path.as_ref().to_path_buf()) {
panic!("CXX-Qt bridge Rust file {} specified in QML module {uri} (version {version_major}.{version_minor}), but also specified via CxxQtBuilder::file. Bridge files must be specified via CxxQtBuilder::file or CxxQtBuilder::qml_module, but not both.", path.as_ref().display());
}
}
self.qml_modules.push(QmlModule {
uri: uri.to_owned(),
version_major,
version_minor,
rust_files: rust_files
.iter()
.map(|p| p.as_ref().to_path_buf())
.collect(),
});
self
}

/// Specify a C++ header containing a Q_OBJECT macro to run [moc](https://doc.qt.io/qt-6/moc.html) on.
/// 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(QObjectHeader {
path: path.to_owned(),
qml_metadata: Vec::new(),
});
self.qobject_headers.push(path.to_owned());
println!("cargo:rerun-if-changed={}", path.display());
self
}
Expand Down Expand Up @@ -467,62 +498,52 @@ impl CxxQtBuilder {
}

// Generate files
match generate_cxxqt_cpp_files(&self.rust_sources, &generated_header_dir) {
Ok(generated_files) => {
for files in generated_files {
self.cc_builder.file(files.plain_cpp);
if let (Some(qobject), Some(qobject_header)) =
(files.qobject, files.qobject_header)
{
self.cc_builder.file(&qobject);
self.qobject_headers.push(qobject_header);
}
}
}
Err(diagnostic) => {
// When CXX-Qt fails in the build script, we shouldn't panic, as the Rust backtrace
// probably isn't useful. We can instead report the error nicely, using
// codespan_reporting and then just exit the build script with a non-zero exit code.
// This will make for a cleaner build-script output than panicing.
diagnostic.report();
std::process::exit(1);
for files in generate_cxxqt_cpp_files(&self.rust_sources, &generated_header_dir) {
self.cc_builder.file(files.plain_cpp);
if let (Some(qobject), Some(qobject_header)) = (files.qobject, files.qobject_header) {
self.cc_builder.file(&qobject);
self.qobject_headers.push(qobject_header);
}
}

// To support multiple QML elements with the same import URI, qmltyperegistrar must be run
// only once for each QML module (URI). So, collect the metadata for all QML elements within
// each module, regardless of which Rust QObject they are from.
let mut qml_modules = HashMap::<(String, usize, usize), Vec<PathBuf>>::new();
let mut cc_builder_whole_archive_files_added = false;
// Run moc on C++ headers with Q_OBJECT macro
for qobject_header in self.qobject_headers {
let uris = qobject_header
.qml_metadata
.iter()
.map(|qml_metadata| qml_metadata.uri.as_str());
let moc_products = qtbuild.moc(&qobject_header.path, uris);
let moc_products = qtbuild.moc(&qobject_header, None);
self.cc_builder.file(moc_products.cpp);
for qml_metadata in qobject_header.qml_metadata {
self.cc_builder.define("QT_STATICPLUGIN", None);
qml_modules
.entry((
qml_metadata.uri.clone(),
qml_metadata.version_major,
qml_metadata.version_minor,
))
.or_default()
.push(moc_products.metatypes_json.clone());
}
}
for ((uri, version_major, version_minor), paths) in qml_modules {
let qml_type_registration_files =
qtbuild.register_qml_types(&paths, version_major, version_minor, &uri);

let mut cc_builder_whole_archive_files_added = false;

// Bridges for QML modules are handled separately because
// the metatypes_json generated by moc needs to be passed to qmltyperegistrar
for qml_module in self.qml_modules {
let mut qml_metatypes_json = Vec::new();

for files in generate_cxxqt_cpp_files(&qml_module.rust_files, &generated_header_dir) {
self.cc_builder.file(files.plain_cpp);
if let (Some(qobject), Some(qobject_header)) = (files.qobject, files.qobject_header)
{
self.cc_builder.file(&qobject);
let moc_products = qtbuild.moc(qobject_header, Some(&qml_module.uri));
self.cc_builder.file(moc_products.cpp);
qml_metatypes_json.push(moc_products.metatypes_json);
}
}

let qml_type_registration_files = qtbuild.register_qml_types(
&qml_metatypes_json,
qml_module.version_major,
qml_module.version_minor,
&qml_module.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);
self.cc_builder.define("QT_STATICPLUGIN", None);
cc_builder_whole_archive_files_added = true;
}

for qrc_file in self.qrc_files {
cc_builder_whole_archive.file(qtbuild.qrc(&qrc_file));
cc_builder_whole_archive_files_added = true;
Expand Down
6 changes: 3 additions & 3 deletions crates/cxx-qt-gen/src/generator/cpp/qobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ mod tests {
#[cxx_qt::bridge(namespace = "cxx_qt")]
mod ffi {
extern "RustQt" {
#[cxx_qt::qobject(qml_uri = "com.kdab", qml_version = "1.0", qml_name = "MyQmlElement")]
#[cxx_qt::qobject(qml_element = "MyQmlElement")]
type MyNamedObject = super::MyNamedObjectRust;
}
}
Expand All @@ -240,7 +240,7 @@ mod tests {
#[cxx_qt::bridge(namespace = "cxx_qt")]
mod ffi {
extern "RustQt" {
#[cxx_qt::qobject(qml_uri = "com.kdab", qml_version = "1.0", qml_singleton)]
#[cxx_qt::qobject(qml_element, qml_singleton)]
type MyObject = super::MyObjectRust;
}
}
Expand All @@ -267,7 +267,7 @@ mod tests {
#[cxx_qt::bridge(namespace = "cxx_qt")]
mod ffi {
extern "RustQt" {
#[cxx_qt::qobject(qml_uri = "com.kdab", qml_version = "1.0", qml_uncreatable)]
#[cxx_qt::qobject(qml_element, qml_uncreatable)]
type MyObject = super::MyObjectRust;
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/cxx-qt-gen/src/generator/rust/qobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ mod tests {
#[cxx_qt::bridge(namespace = "cxx_qt")]
mod ffi {
extern "RustQt" {
#[cxx_qt::qobject(qml_uri = "com.kdab", qml_version = "1.0", qml_singleton)]
#[cxx_qt::qobject(qml_element, qml_singleton)]
type MyObject = super::MyObjectRust;
}
}
Expand Down
Loading

0 comments on commit 273a4a5

Please sign in to comment.