diff --git a/CHANGELOG.md b/CHANGELOG.md index 60a7658a3..ce53a5239 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for further types: `qreal`, `qint64`, `qintptr`, `qsizetype`, `quint64`, `quintptr` - Allow creating a `QImage` from an `image::RgbaImage`. - Support for `cfg` attributes through to C++ generation +- CXX-Qt-build: Improved compile time and propagation of initializers between crates ### Fixed @@ -33,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed - CXX-Qt-build: Interface no longer includes compiler definitions () +- CXX-Qt-build: Interface no longer includes initializers ## [0.7.0](https://github.com/KDAB/cxx-qt/compare/v0.6.1...v0.7.0) - 2024-10-30 diff --git a/book/src/internals/build-system.md b/book/src/internals/build-system.md index e60c8028a..1ebc61637 100644 --- a/book/src/internals/build-system.md +++ b/book/src/internals/build-system.md @@ -19,13 +19,34 @@ Qt code often contains initialization code that is called by a static variable t However, when linking into a static library, and then linking into the main executable, the linker will discard everything from the library that isn't used by the main executable, including these static initializers, as they're never actually used and just exist to run their constructor code. -There are two ways to solve this: +There are multiple ways to solve this: - Export an object file and link that to the main binary. Object files are always included completely - Use the whole-archive linker flag which forces inclusion of every object within the static library. - If we include the entire static lib generated by cargo, then we'll likely get duplicate symbols, as this really includes **everything** that your Rust code **may** need, even if you don't use it. - This has caused some recent regressions with Rust 1.78+, where MSVC could no longer link CXX-Qt due to duplicate symbols - The way to solve this is to only export the static initializers as a library and link that into CMake. +- Manually calling the static initializer code + - This is basically what Q_INIT_RESOURCE and Q_IMPORT_PLUGIN do + - They call the registration method directly, which circumvents the static initializers and forces the static initializers to be linked if they would otherwise be discarded. + +At the moment we employ a mix of all methods. + +First and foremost, we wrap all our initializers into functions with well-defined names (starting with `cxx_qt_init`) and C-compatible signatures. +This allows us to manually call the initializers from any point in the linker chain, which forces their inclusion. +These initializer functions call the initializer functions from their upstream dependencies so that the entire dependency tree is initialized. + +However, we don't want to have to call the initializers manually in every resulting binary. +To solve this, we use static initializers that simply call the initializer function of the crate/Qml module, thereby initializing all dependencies. +As noted earlier, these static initializers are routinely optimized out by the linker. + +For Cargo builds we prevent this by linking all initializers with +whole-archive which forces all of them to be included. +Experience has shown that this gives us the best compatibility overall, as linking object files to Cargo builds turned out to be quite finicky. +As the initializers contain very few symbols themselves, this should also rarely lead to issues with duplicate symbols. + +In CMake we mirror Qts behavior, which is to build the static initializer as an `OBJECT` library. +The initializer functions themselves are still built into the Rust static library and the `OBJECT` library must therefore link to it. +This is taken care of by the `cxx_qt_import_crate`/`_import_qml_module` functions. ### Header files @@ -87,7 +108,7 @@ However, we also want to provide some custom functions that wrap corrosion and s Currently we provide two functions: -- cxxqt_import_crate +- cxx_qt_import_crate - A wrapper over corrosion_import_crate that defines the `CXXQT_EXPORT_DIR`, imports the initializers object files, etc. -- cxxqt_import_qml_module +- cxx_qt_import_qml_module - Import a given QML module by URI from the given SOURCE_CRATE and provide it as a target. diff --git a/crates/cxx-qt-build/Cargo.toml b/crates/cxx-qt-build/Cargo.toml index 618b37fea..d58970138 100644 --- a/crates/cxx-qt-build/Cargo.toml +++ b/crates/cxx-qt-build/Cargo.toml @@ -19,10 +19,10 @@ cxx-gen.workspace = true cxx-qt-gen.workspace = true proc-macro2.workspace = true quote.workspace = true -qt-build-utils.workspace = true +qt-build-utils = { workspace = true, features = ["serde"] } codespan-reporting = "0.11" version_check = "0.9" -serde = { version = "1.0", features = ["default", "derive"] } +serde.workspace = true serde_json = "1.0" [features] diff --git a/crates/cxx-qt-build/src/dependencies.rs b/crates/cxx-qt-build/src/dependencies.rs index 7f4c10b20..659e55f55 100644 --- a/crates/cxx-qt-build/src/dependencies.rs +++ b/crates/cxx-qt-build/src/dependencies.rs @@ -13,7 +13,6 @@ use std::path::{Path, PathBuf}; /// When generating a library with cxx-qt-build, the library may need to export certain flags or headers. /// These are all specified by this Interface struct, which should be passed to the [crate::CxxQtBuilder::library] function. pub struct Interface { - pub(crate) initializers: Vec, // The name of the links keys, whose CXX-Qt dependencies to reexport pub(crate) reexport_links: HashSet, pub(crate) exported_include_prefixes: Vec, @@ -28,7 +27,6 @@ pub struct Interface { impl Default for Interface { fn default() -> Self { Self { - initializers: Vec::new(), reexport_links: HashSet::new(), exported_include_prefixes: vec![super::crate_name()], exported_include_directories: Vec::new(), @@ -37,21 +35,6 @@ impl Default for Interface { } impl Interface { - /// Add a C++ file path that will be exported as an initializer to downstream dependencies. - /// - /// Initializer files will be built into object files, instead of linked into the static - /// library. - /// This way, the static variables and their constructors in this code will not be optimized - /// out by the linker. - pub fn initializer(mut self, path: impl AsRef) -> Self { - let path = PathBuf::from(path.as_ref()); - let path = path - .canonicalize() - .expect("Failed to canonicalize path to initializer! Does the path exist?"); - self.initializers.push(path); - self - } - /// Export all headers with the given prefix to downstream dependencies /// /// Note: This will overwrite any previously specified header_prefixes, including the default @@ -124,7 +107,7 @@ pub(crate) struct Manifest { pub(crate) name: String, pub(crate) link_name: String, pub(crate) qt_modules: Vec, - pub(crate) initializers: Vec, + pub(crate) initializers: Vec, pub(crate) exported_include_prefixes: Vec, } @@ -173,18 +156,10 @@ impl Dependency { } } -pub(crate) fn initializer_paths( - interface: Option<&Interface>, - dependencies: &[Dependency], -) -> HashSet { +pub(crate) fn initializers(dependencies: &[Dependency]) -> Vec { dependencies .iter() .flat_map(|dep| dep.manifest.initializers.iter().cloned()) - .chain( - interface - .iter() - .flat_map(|interface| interface.initializers.iter().cloned()), - ) .collect() } diff --git a/crates/cxx-qt-build/src/dir.rs b/crates/cxx-qt-build/src/dir.rs index 666ac3f2e..71641468e 100644 --- a/crates/cxx-qt-build/src/dir.rs +++ b/crates/cxx-qt-build/src/dir.rs @@ -91,6 +91,12 @@ pub(crate) fn is_exporting() -> bool { export().is_some() } +pub(crate) fn initializers(key: &str) -> PathBuf { + let path = out().join("cxx-qt-build").join("initializers").join(key); + std::fs::create_dir_all(&path).expect("Failed to create initializers path!"); + path +} + #[cfg(unix)] pub(crate) fn symlink_or_copy_directory( source: impl AsRef, diff --git a/crates/cxx-qt-build/src/lib.rs b/crates/cxx-qt-build/src/lib.rs index c76f4f138..5932a0245 100644 --- a/crates/cxx-qt-build/src/lib.rs +++ b/crates/cxx-qt-build/src/lib.rs @@ -34,6 +34,7 @@ pub use qml_modules::QmlModule; pub use qt_build_utils::MocArguments; use qt_build_utils::SemVer; use quote::ToTokens; +use std::iter; use std::{ collections::HashSet, env, @@ -310,6 +311,14 @@ fn static_lib_name() -> String { format!("{}-cxxqt-generated", crate_name()) } +fn crate_init_key() -> String { + format!("crate_{}", crate_name().replace('-', "_")) +} + +fn qml_module_init_key(module_uri: &str) -> String { + format!("qml_module_{}", module_name_from_uri(module_uri)) +} + fn panic_duplicate_file_and_qml_module( path: impl AsRef, uri: &str, @@ -356,12 +365,12 @@ pub struct CxxQtBuilder { rust_sources: Vec, qobject_headers: Vec, qrc_files: Vec, + init_files: Vec, qt_modules: HashSet, qml_modules: Vec, cc_builder: cc::Build, public_interface: Option, include_prefix: String, - initializers: Vec, } impl CxxQtBuilder { @@ -396,10 +405,10 @@ impl CxxQtBuilder { rust_sources: vec![], qobject_headers: vec![], qrc_files: vec![], + init_files: vec![], qt_modules, qml_modules: vec![], cc_builder: cc::Build::new(), - initializers: vec![], public_interface: None, include_prefix: crate_name(), } @@ -439,6 +448,15 @@ impl CxxQtBuilder { self } + #[doc(hidden)] + pub fn initializer(mut self, initializer: qt_build_utils::Initializer) -> Self { + if let Some(ref init_file) = initializer.file { + println!("cargo::rerun-if-changed={}", init_file.display()); + } + self.init_files.push(initializer); + self + } + /// Include files listed in a .qrc file into the binary /// with [Qt's resource system](https://doc.qt.io/qt-6/resources.html). /// ```no_run @@ -705,38 +723,34 @@ impl CxxQtBuilder { } } - fn build_object_file(builder: &cc::Build, file_path: impl AsRef, object_path: PathBuf) { + fn export_object_file(builder: &cc::Build, file_path: impl AsRef, export_path: PathBuf) { let mut obj_builder = builder.clone(); - obj_builder.file(file_path); - let obj_files = obj_builder.compile_intermediates(); + obj_builder.file(file_path.as_ref()); // We only expect a single file, so destructure the vec. // If there's 0 or > 1 file, we panic in the `else` branch, because then the builder is // probably not correctly configured. + let obj_files = obj_builder.compile_intermediates(); if let [obj_file] = &obj_files[..] { - if dir::is_exporting() { - if let Some(directory) = object_path.parent() { - std::fs::create_dir_all(directory).unwrap_or_else(|_| { - panic!( - "Could not create directory for object file: {}", - object_path.to_string_lossy() - ) - }); - } - std::fs::copy(obj_file, &object_path).unwrap_or_else(|_| { + if let Some(directory) = export_path.parent() { + std::fs::create_dir_all(directory).unwrap_or_else(|_| { panic!( - "Failed to move object file to {}!", - object_path.to_string_lossy() + "Could not create directory for exporting object file: {}", + export_path.to_string_lossy() ) }); - } else { - println!("cargo::rustc-link-arg={}", obj_file.to_string_lossy()); } + std::fs::copy(obj_file, &export_path).unwrap_or_else(|_| { + panic!( + "Failed to export object file to {}!", + export_path.to_string_lossy() + ) + }); } else { panic!( - "CXX-Qt internal error: Expected only one object file out of cc::Build! Got {}", - obj_files.len() - ); + "CXX-Qt internal error: Expected only one object file for export out of cc::Build! Got {}", + obj_files.len() + ); } } @@ -746,7 +760,8 @@ impl CxxQtBuilder { qtbuild: &mut qt_build_utils::QtBuild, generated_header_dir: impl AsRef, header_prefix: &str, - ) { + ) -> Vec { + let mut initializer_functions = Vec::new(); for qml_module in &self.qml_modules { dir::clean(dir::module_target(&qml_module.uri)) .expect("Failed to clean qml module export directory!"); @@ -866,71 +881,141 @@ impl CxxQtBuilder { println!("cargo::rerun-if-changed={}", path.display()); } - // Now all necessary symbols should be included in the cc_builder. - // However, the plugin needs to be initialized at runtime. - // This is done through the plugin_init file. - // It needs to be linked as an object file, to ensure that the linker doesn't throw away - // the static initializers in this file. - // For CMake builds, we export this file to then later include it as an object library in - // CMake. - // In cargo builds, add the object file as a direct argument to the linker. - Self::build_object_file( + let module_init_key = qml_module_init_key(&qml_module.uri); + let private_initializers = [qml_module_registration_files.plugin_init]; + let public_initializer = + Self::generate_public_initializer(&private_initializers, &module_init_key); + Self::build_initializers( init_builder, - &qml_module_registration_files.plugin_init, + &private_initializers, + &public_initializer, dir::module_target(&qml_module.uri).join("plugin_init.o"), + &module_init_key, ); - } - } - fn setup_qt5_compatibility(&mut self, qtbuild: &qt_build_utils::QtBuild) { - // If we are using Qt 5 then write the std_types source - // This registers std numbers as a type for use in QML - // - // Note that we need this to be compiled into an object file - // as they are stored in statics in the source. - // - // TODO: Can we move this into cxx-qt so that it's only built - // once rather than for every cxx-qt-build? When we do this - // ensure that in a multi project that numbers work everywhere. - // - // Also then it should be possible to use CARGO_MANIFEST_DIR/src/std_types_qt5.cpp - // as path for cc::Build rather than copying the .cpp file - // - // https://github.com/rust-lang/rust/issues/108081 - // https://github.com/KDAB/cxx-qt/pull/598 - if qtbuild.version().major == 5 { - self.initializers - .push(include_str!("std_types_qt5.cpp").to_owned()); + initializer_functions.push(public_initializer); } + initializer_functions } - fn generate_init_code(&self, initializers: &HashSet) -> String { - initializers + /// Generate the public initializer. + /// It will call all the private initializers. + /// + /// Downstream crates can therefore just call the initializer once to initialize this crate. + fn generate_public_initializer( + private_initializers: &[qt_build_utils::Initializer], + key: &str, + ) -> qt_build_utils::Initializer { + let (declarations, calls): (Vec<_>, Vec<_>) = private_initializers .iter() - .map(|path| std::fs::read_to_string(path).expect("Could not read initializer file!")) - .chain(self.initializers.iter().cloned()) - .collect::>() - .join("\n") + .map(|initializer| { + ( + // declaration + initializer.init_declaration.clone().unwrap_or_default(), + // call + initializer.init_call.clone().unwrap_or_default(), + ) + }) + .unzip(); + + let init_fun = format!("cxx_qt_init_{key}"); + // For the init_function, we need to use an internal function that is not + // `extern "C"` as Q_INIT_RESOURCES needs name mangling, which doesn't happen if it's + // called within an `extern "C"` function. + // So add a static do_init function that we then call from the actual initializer function. + let init_function = format!( + r#" +#include + +{declarations} + +static bool do_init() {{ + static std::once_flag flag; + std::call_once(flag, []() {{ + {calls} + }}); + return true; +}} + +extern "C" bool {init_fun}() {{ + return do_init(); +}} + "#, + declarations = declarations.join("\n"), + calls = calls.join("\n"), + ); + let init_function_path = dir::initializers(key).join("public-initializer.cpp"); + std::fs::write(&init_function_path, init_function) + .expect("Failed to write public initializer file!"); + + qt_build_utils::Initializer { + file: Some(init_function_path), + ..qt_build_utils::Initializer::default_signature(&init_fun) + } } - fn build_initializers(&mut self, init_builder: &cc::Build, initializers: &HashSet) { - let initializers_path = dir::out().join("cxx-qt-build").join("initializers"); - std::fs::create_dir_all(&initializers_path).expect("Failed to create initializers path!"); + fn build_initializers<'a>( + init_builder: &cc::Build, + private_initializers: impl IntoIterator, + public_initializer: &qt_build_utils::Initializer, + export_path: PathBuf, + key: &str, + ) { + let mut init_lib = init_builder.clone(); + + // Build static initializers into their own library which will be linked with whole-archive. + init_lib + .file( + public_initializer + .file + .as_ref() + .expect("Public initializer must have a file!"), + ) + .files( + private_initializers + .into_iter() + .filter_map(|initializer| initializer.file.as_ref()), + ); - let initializers_path = initializers_path.join(format!("{}.cpp", crate_name())); - std::fs::write(&initializers_path, self.generate_init_code(initializers)) - .expect("Could not write initializers file"); - Self::build_object_file( - init_builder, - initializers_path, - dir::crate_target().join("initializers.o"), + let init_call = format!( + "{declaration}\nstatic const bool do_init_{key} = {init_call}", + declaration = public_initializer + .init_declaration + .clone() + .unwrap_or_default(), + init_call = public_initializer + .init_call + .clone() + .expect("Public initializer must be callable!"), ); + + let init_file = dir::initializers(key).join("call-initializers.cpp"); + std::fs::write(&init_file, init_call).expect("Could not write initializers call file!"); + + if dir::is_exporting() { + Self::export_object_file(init_builder, init_file, export_path); + } else { + init_lib.file(init_file); + } + + // Link the init_lib with +whole-archive to ensure that the static initializers are not discarded. + // We previously used object files that we linked directly into the final binary, but this caused + // issues, as the static initializers could sometimes not link to the initializer functions. + // This is simpler and ends up linking correctly. + // + // The trick is that we only link the initializers with +whole-archive, and not the entire + // Rust static library, as the initializers are rather simple and shouldn't lead to issues with + // duplicate symbols. + // Note that for CMake builds we still need to export an object file to link to. + init_lib + .link_lib_modifier("+whole-archive") + .compile(&format!("cxx-qt-init-lib-{}", key)); } fn generate_cpp_from_qrc_files( &mut self, qtbuild: &mut qt_build_utils::QtBuild, - ) -> HashSet { + ) -> Vec { self.qrc_files .iter() .map(|qrc_file| { @@ -949,16 +1034,15 @@ impl CxxQtBuilder { &self, dependencies: &[Dependency], qt_modules: HashSet, - initializers: HashSet, + initializers: Vec, ) { if let Some(interface) = &self.public_interface { - // We automatically reexport all qt_modules and initializers from downstream dependencies + // We automatically reexport all qt_modules and downstream dependencies // as they will always need to be enabled in the final binary. - // However, we only reexport the headers and compile-time definitions of libraries that + // However, we only reexport the headers of libraries that // are marked as re-export. let dependencies = dependencies::reexported_dependencies(interface, dependencies); - let initializers = initializers.into_iter().collect(); let exported_include_prefixes = dependencies::all_include_prefixes(interface, &dependencies); @@ -1067,37 +1151,52 @@ impl CxxQtBuilder { // Bridges for QML modules are handled separately because // the metatypes_json generated by moc needs to be passed to qmltyperegistrar - self.build_qml_modules( + let module_initializers = self.build_qml_modules( &init_builder, &mut qtbuild, &header_root, &self.include_prefix.clone(), ); - let mut initializers = self.generate_cpp_from_qrc_files(&mut qtbuild); - initializers.extend(dependencies::initializer_paths( - self.public_interface.as_ref(), - &dependencies, - )); + let qrc_files = self.generate_cpp_from_qrc_files(&mut qtbuild); - self.setup_qt5_compatibility(&qtbuild); + let dependency_initializers = dependencies::initializers(&dependencies); + let private_initializers = dependency_initializers + .into_iter() + .chain(qrc_files) + .chain(self.init_files.iter().cloned()) + .collect::>(); - self.build_initializers(&init_builder, &initializers); + let public_initializer = + Self::generate_public_initializer(&private_initializers, &crate_init_key()); + Self::build_initializers( + &init_builder, + &private_initializers, + &public_initializer, + dir::crate_target().join("initializers.o"), + &crate_init_key(), + ); // Only compile if we have added files to the builder // otherwise we end up with no static library but ask cargo to link to it which causes an error if self.cc_builder.get_files().count() > 0 { - // The linker argument order matters! - // We need to link the object file first, then link the static library. - // Otherwise, the linker will be unable to find the symbols in the static library file. - // See also: https://stackoverflow.com/questions/45135/why-does-the-order-in-which-libraries-are-linked-sometimes-cause-errors-in-gcc - if !dir::is_exporting() { - println!("cargo::rustc-link-arg=-l{}", static_lib_name()); - } - self.cc_builder.compile(&static_lib_name()); } - self.write_manifest(&dependencies, qt_modules, initializers); + self.write_manifest( + &dependencies, + qt_modules, + module_initializers + .into_iter() + .chain(iter::once(public_initializer)) + // Strip the init files from the public initializers + // For downstream dependencies, it's enough to just declare the init function an + // call it. + .map(|initializer| qt_build_utils::Initializer { + file: None, + ..initializer + }) + .collect(), + ); } } diff --git a/crates/cxx-qt-build/src/std_types_qt5.cpp b/crates/cxx-qt-build/src/std_types_qt5.cpp deleted file mode 100644 index 1075f3a54..000000000 --- a/crates/cxx-qt-build/src/std_types_qt5.cpp +++ /dev/null @@ -1,42 +0,0 @@ -// clang-format off -// SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company -// clang-format on -// SPDX-FileContributor: Andrew Hayzen -// -// SPDX-License-Identifier: MIT OR Apache-2.0 - -#include - -// For versions less than Qt 6 we need to manually register the std numerics -#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) -#include - -#include - -namespace rust { -namespace cxxqtlib1 { - -// Ensure that std int types are registered -// so that they can be used with QML in Qt 5 - -static const int register_i8 = - qRegisterMetaType<::std::int8_t>("::std::int8_t"); -static const int register_i16 = - qRegisterMetaType<::std::int16_t>("::std::int16_t"); -static const int register_i32 = - qRegisterMetaType<::std::int32_t>("::std::int32_t"); -static const int register_i64 = - qRegisterMetaType<::std::int64_t>("::std::int64_t"); - -static const int register_u8 = - qRegisterMetaType<::std::uint8_t>("::std::uint8_t"); -static const int register_u16 = - qRegisterMetaType<::std::uint16_t>("::std::uint16_t"); -static const int register_u32 = - qRegisterMetaType<::std::uint32_t>("::std::uint32_t"); -static const int register_u64 = - qRegisterMetaType<::std::uint64_t>("::std::uint64_t"); - -} -} -#endif diff --git a/crates/cxx-qt-lib/build.rs b/crates/cxx-qt-lib/build.rs index db041663e..bea0d7d45 100644 --- a/crates/cxx-qt-lib/build.rs +++ b/crates/cxx-qt-lib/build.rs @@ -348,20 +348,25 @@ fn main() { cpp_files.extend(["core/qdatetime", "core/qtimezone"]); } - let mut interface = cxx_qt_build::Interface::default() - .initializer("src/core/init.cpp") + let interface = cxx_qt_build::Interface::default() .export_include_prefixes([]) .export_include_directory(header_dir(), "cxx-qt-lib") .reexport_dependency("cxx-qt"); - if qt_gui_enabled() { - interface = interface.initializer("src/gui/init.cpp"); - } - - let mut builder = CxxQtBuilder::library(interface).include_prefix("cxx-qt-lib-internals"); + let mut builder = CxxQtBuilder::library(interface) + .include_prefix("cxx-qt-lib-internals") + .initializer(qt_build_utils::Initializer { + file: Some("src/core/init.cpp".into()), + ..qt_build_utils::Initializer::default_signature("init_cxx_qt_lib_core") + }); if qt_gui_enabled() { - builder = builder.qt_module("Gui"); + builder = builder + .qt_module("Gui") + .initializer(qt_build_utils::Initializer { + file: Some("src/gui/init.cpp".into()), + ..qt_build_utils::Initializer::default_signature("init_cxx_qt_lib_gui") + }); } if qt_qml_enabled() { diff --git a/crates/cxx-qt-lib/src/core/init.cpp b/crates/cxx-qt-lib/src/core/init.cpp index 34221f2b2..ef6425a39 100644 --- a/crates/cxx-qt-lib/src/core/init.cpp +++ b/crates/cxx-qt-lib/src/core/init.cpp @@ -11,169 +11,111 @@ #include #include -static const int register_QHash_i32_QByteArray = - qRegisterMetaType<::QHash_i32_QByteArray>("QHash_i32_QByteArray"); -// Ensure that QHash (aka QVariantHash) is registered -// otherwise it cannot be used in QML -static const int register_QHash_QString_QVariant = - qRegisterMetaType<::QHash_QString_QVariant>("QHash_QString_QVariant"); +#include -static const int register_QList_bool = - qRegisterMetaType<::QList_bool>("QList_bool"); -static const int register_QList_f32 = - qRegisterMetaType<::QList_f32>("QList_f32"); -static const int register_QList_f64 = - qRegisterMetaType<::QList_f64>("QList_f64"); -static const int register_QList_i8 = qRegisterMetaType<::QList_i8>("QList_i8"); -static const int register_QList_i16 = - qRegisterMetaType<::QList_i16>("QList_i16"); -static const int register_QList_i32 = - qRegisterMetaType<::QList_i32>("QList_i32"); -static const int register_QList_i64 = - qRegisterMetaType<::QList_i64>("QList_i64"); -static const int register_QList_QByteArray = - qRegisterMetaType<::QList_QByteArray>("QList_QByteArray"); -static const int register_QList_QDate = - qRegisterMetaType<::QList_QDate>("QList_QDate"); -static const int register_QList_QDateTime = - qRegisterMetaType<::QList_QDateTime>("QList_QDateTime"); -static const int register_QList_QLine = - qRegisterMetaType<::QList_QLine>("QList_QLine"); -static const int register_QList_QLineF = - qRegisterMetaType<::QList_QLineF>("QList_QLineF"); -static const int register_QList_QMargins = - qRegisterMetaType<::QList_QMargins>("QList_QMargins"); -static const int register_QList_QMarginsF = - qRegisterMetaType<::QList_QMarginsF>("QList_QMarginsF"); -static const int register_QList_QPersistentModelIndex = - qRegisterMetaType<::QList_QPersistentModelIndex>( - "QList_QPersistentModelIndex"); -static const int register_QList_QPoint = - qRegisterMetaType<::QList_QPoint>("QList_QPoint"); -static const int register_QList_QPointF = - qRegisterMetaType<::QList_QPointF>("QList_QPointF"); -static const int register_QList_QRect = - qRegisterMetaType<::QList_QRect>("QList_QRect"); -static const int register_QList_QRectF = - qRegisterMetaType<::QList_QRectF>("QList_QRectF"); -static const int register_QList_QSize = - qRegisterMetaType<::QList_QSize>("QList_QSize"); -static const int register_QList_QSizeF = - qRegisterMetaType<::QList_QSizeF>("QList_QSizeF"); -static const int register_QList_QString = - qRegisterMetaType<::QList_QString>("QList_QString"); -static const int register_QList_QTime = - qRegisterMetaType<::QList_QTime>("QList_QTime"); -static const int register_QList_QUrl = - qRegisterMetaType<::QList_QUrl>("QList_QUrl"); -static const int register_QList_QUuid = - qRegisterMetaType<::QList_QUuid>("QList_QUuid"); -// Ensure that QList (aka QVariantList) is registered -// otherwise it cannot be used in QML -static const int register_QList_QVariant = - qRegisterMetaType<::QList_QVariant>("QList_QVariant"); -static const int register_QList_u8 = qRegisterMetaType<::QList_u8>("QList_u8"); -static const int register_QList_u16 = - qRegisterMetaType<::QList_u16>("QList_u16"); -static const int register_QList_u32 = - qRegisterMetaType<::QList_u32>("QList_u32"); -static const int register_QList_u64 = - qRegisterMetaType<::QList_u64>("QList_u64"); +extern "C" bool +init_cxx_qt_lib_core() +{ + static std::once_flag flag; + std::call_once(flag, []() { + qRegisterMetaType<::QHash_i32_QByteArray>("QHash_i32_QByteArray"); + // Ensure that QHash (aka QVariantHash) is registered + // otherwise it cannot be used in QML + qRegisterMetaType<::QHash_QString_QVariant>("QHash_QString_QVariant"); -// Ensure that QMap (aka QVariantMap) is registered -// otherwise it cannot be used in QML -static const int register_QMap_QString_QVariant = - qRegisterMetaType<::QMap_QString_QVariant>("QMap_QString_QVariant"); + qRegisterMetaType<::QList_bool>("QList_bool"); + qRegisterMetaType<::QList_f32>("QList_f32"); + qRegisterMetaType<::QList_f64>("QList_f64"); + qRegisterMetaType<::QList_i8>("QList_i8"); + qRegisterMetaType<::QList_i16>("QList_i16"); + qRegisterMetaType<::QList_i32>("QList_i32"); + qRegisterMetaType<::QList_i64>("QList_i64"); + qRegisterMetaType<::QList_QByteArray>("QList_QByteArray"); + qRegisterMetaType<::QList_QDate>("QList_QDate"); + qRegisterMetaType<::QList_QDateTime>("QList_QDateTime"); + qRegisterMetaType<::QList_QLine>("QList_QLine"); + qRegisterMetaType<::QList_QLineF>("QList_QLineF"); + qRegisterMetaType<::QList_QMargins>("QList_QMargins"); + qRegisterMetaType<::QList_QMarginsF>("QList_QMarginsF"); + qRegisterMetaType<::QList_QPersistentModelIndex>( + "QList_QPersistentModelIndex"); + qRegisterMetaType<::QList_QPoint>("QList_QPoint"); + qRegisterMetaType<::QList_QPointF>("QList_QPointF"); + qRegisterMetaType<::QList_QRect>("QList_QRect"); + qRegisterMetaType<::QList_QRectF>("QList_QRectF"); + qRegisterMetaType<::QList_QSize>("QList_QSize"); + qRegisterMetaType<::QList_QSizeF>("QList_QSizeF"); + qRegisterMetaType<::QList_QString>("QList_QString"); + qRegisterMetaType<::QList_QTime>("QList_QTime"); + qRegisterMetaType<::QList_QUrl>("QList_QUrl"); + qRegisterMetaType<::QList_QUuid>("QList_QUuid"); + // Ensure that QList (aka QVariantList) is registered + // otherwise it cannot be used in QML + qRegisterMetaType<::QList_QVariant>("QList_QVariant"); + qRegisterMetaType<::QList_u8>("QList_u8"); + qRegisterMetaType<::QList_u16>("QList_u16"); + qRegisterMetaType<::QList_u32>("QList_u32"); + qRegisterMetaType<::QList_u64>("QList_u64"); -static const int register_QSet_bool = - qRegisterMetaType<::QSet_bool>("QSet_bool"); -static const int register_QSet_f32 = qRegisterMetaType<::QSet_f32>("QSet_f32"); -static const int register_QSet_f64 = qRegisterMetaType<::QSet_f64>("QSet_f64"); -static const int register_QSet_i8 = qRegisterMetaType<::QSet_i8>("QSet_i8"); -static const int register_QSet_i16 = qRegisterMetaType<::QSet_i16>("QSet_i16"); -static const int register_QSet_i32 = qRegisterMetaType<::QSet_i32>("QSet_i32"); -static const int register_QSet_i64 = qRegisterMetaType<::QSet_i64>("QSet_i64"); -static const int register_QSet_QByteArray = - qRegisterMetaType<::QSet_QByteArray>("QSet_QByteArray"); -static const int register_QSet_QDate = - qRegisterMetaType<::QSet_QDate>("QSet_QDate"); -static const int register_QSet_QDateTime = - qRegisterMetaType<::QSet_QDateTime>("QSet_QDateTime"); -static const int register_QSet_QPersistentModelIndex = - qRegisterMetaType<::QSet_QPersistentModelIndex>("QSet_QPersistentModelIndex"); -static const int register_QSet_QString = - qRegisterMetaType<::QSet_QString>("QSet_QString"); -static const int register_QSet_QTime = - qRegisterMetaType<::QSet_QTime>("QSet_QTime"); -static const int register_QSet_QUrl = - qRegisterMetaType<::QSet_QUrl>("QSet_QUrl"); -static const int register_QSet_QUuid = - qRegisterMetaType<::QSet_QUuid>("QSet_QUuid"); -static const int register_QSet_u8 = qRegisterMetaType<::QSet_u8>("QSet_u8"); -static const int register_QSet_u16 = qRegisterMetaType<::QSet_u16>("QSet_u16"); -static const int register_QSet_u32 = qRegisterMetaType<::QSet_u32>("QSet_u32"); -static const int register_QSet_u64 = qRegisterMetaType<::QSet_u64>("QSet_u64"); + // Ensure that QMap (aka QVariantMap) is registered + // otherwise it cannot be used in QML + qRegisterMetaType<::QMap_QString_QVariant>("QMap_QString_QVariant"); -static const int register_QVector_bool = - qRegisterMetaType<::QVector_bool>("QVector_bool"); -static const int register_QVector_f32 = - qRegisterMetaType<::QVector_f32>("QVector_f32"); -static const int register_QVector_f64 = - qRegisterMetaType<::QVector_f64>("QVector_f64"); -static const int register_QVector_i8 = - qRegisterMetaType<::QVector_i8>("QVector_i8"); -static const int register_QVector_i16 = - qRegisterMetaType<::QVector_i16>("QVector_i16"); -static const int register_QVector_i32 = - qRegisterMetaType<::QVector_i32>("QVector_i32"); -static const int register_QVector_i64 = - qRegisterMetaType<::QVector_i64>("QVector_i64"); -static const int register_QVector_QByteArray = - qRegisterMetaType<::QVector_QByteArray>("QVector_QByteArray"); -static const int register_QVector_QDate = - qRegisterMetaType<::QVector_QDate>("QVector_QDate"); -static const int register_QVector_QDateTime = - qRegisterMetaType<::QVector_QDateTime>("QVector_QDateTime"); -static const int register_QVector_QLine = - qRegisterMetaType<::QVector_QLine>("QVector_QLine"); -static const int register_QVector_QLineF = - qRegisterMetaType<::QVector_QLineF>("QVector_QLineF"); -static const int register_QVector_QMargins = - qRegisterMetaType<::QVector_QMargins>("QVector_QMargins"); -static const int register_QVector_QMarginsF = - qRegisterMetaType<::QVector_QMarginsF>("QVector_QMarginsF"); -static const int register_QVector_QPersistentModelIndex = - qRegisterMetaType<::QVector_QPersistentModelIndex>( - "QVector_QPersistentModelIndex"); -static const int register_QVector_QPoint = - qRegisterMetaType<::QVector_QPoint>("QVector_QPoint"); -static const int register_QVector_QPointF = - qRegisterMetaType<::QVector_QPointF>("QVector_QPointF"); -static const int register_QVector_QRect = - qRegisterMetaType<::QVector_QRect>("QVector_QRect"); -static const int register_QVector_QRectF = - qRegisterMetaType<::QVector_QRectF>("QVector_QRectF"); -static const int register_QVector_QSize = - qRegisterMetaType<::QVector_QSize>("QVector_QSize"); -static const int register_QVector_QSizeF = - qRegisterMetaType<::QVector_QSizeF>("QVector_QSizeF"); -static const int register_QVector_QString = - qRegisterMetaType<::QVector_QString>("QVector_QString"); -static const int register_QVector_QTime = - qRegisterMetaType<::QVector_QTime>("QVector_QTime"); -static const int register_QVector_QUrl = - qRegisterMetaType<::QVector_QUrl>("QVector_QUrl"); -static const int register_QVector_QUuid = - qRegisterMetaType<::QVector_QUuid>("QVector_QUuid"); -// Ensure that QVector (aka QVariantList) is registered -// otherwise it cannot be used in QML -static const int register_QVector_QVariant = - qRegisterMetaType<::QVector_QVariant>("QVector_QVariant"); -static const int register_QVector_u8 = - qRegisterMetaType<::QVector_u8>("QVector_u8"); -static const int register_QVector_u16 = - qRegisterMetaType<::QVector_u16>("QVector_u16"); -static const int register_QVector_u32 = - qRegisterMetaType<::QVector_u32>("QVector_u32"); -static const int register_QVector_u64 = - qRegisterMetaType<::QVector_u64>("QVector_u64"); + qRegisterMetaType<::QSet_bool>("QSet_bool"); + qRegisterMetaType<::QSet_f32>("QSet_f32"); + qRegisterMetaType<::QSet_f64>("QSet_f64"); + qRegisterMetaType<::QSet_i8>("QSet_i8"); + qRegisterMetaType<::QSet_i16>("QSet_i16"); + qRegisterMetaType<::QSet_i32>("QSet_i32"); + qRegisterMetaType<::QSet_i64>("QSet_i64"); + qRegisterMetaType<::QSet_QByteArray>("QSet_QByteArray"); + qRegisterMetaType<::QSet_QDate>("QSet_QDate"); + qRegisterMetaType<::QSet_QDateTime>("QSet_QDateTime"); + qRegisterMetaType<::QSet_QPersistentModelIndex>( + "QSet_QPersistentModelIndex"); + qRegisterMetaType<::QSet_QString>("QSet_QString"); + qRegisterMetaType<::QSet_QTime>("QSet_QTime"); + qRegisterMetaType<::QSet_QUrl>("QSet_QUrl"); + qRegisterMetaType<::QSet_QUuid>("QSet_QUuid"); + qRegisterMetaType<::QSet_u8>("QSet_u8"); + qRegisterMetaType<::QSet_u16>("QSet_u16"); + qRegisterMetaType<::QSet_u32>("QSet_u32"); + qRegisterMetaType<::QSet_u64>("QSet_u64"); + + qRegisterMetaType<::QVector_bool>("QVector_bool"); + qRegisterMetaType<::QVector_f32>("QVector_f32"); + qRegisterMetaType<::QVector_f64>("QVector_f64"); + qRegisterMetaType<::QVector_i8>("QVector_i8"); + qRegisterMetaType<::QVector_i16>("QVector_i16"); + qRegisterMetaType<::QVector_i32>("QVector_i32"); + qRegisterMetaType<::QVector_i64>("QVector_i64"); + qRegisterMetaType<::QVector_QByteArray>("QVector_QByteArray"); + qRegisterMetaType<::QVector_QDate>("QVector_QDate"); + qRegisterMetaType<::QVector_QDateTime>("QVector_QDateTime"); + qRegisterMetaType<::QVector_QLine>("QVector_QLine"); + qRegisterMetaType<::QVector_QLineF>("QVector_QLineF"); + qRegisterMetaType<::QVector_QMargins>("QVector_QMargins"); + qRegisterMetaType<::QVector_QMarginsF>("QVector_QMarginsF"); + qRegisterMetaType<::QVector_QPersistentModelIndex>( + "QVector_QPersistentModelIndex"); + qRegisterMetaType<::QVector_QPoint>("QVector_QPoint"); + qRegisterMetaType<::QVector_QPointF>("QVector_QPointF"); + qRegisterMetaType<::QVector_QRect>("QVector_QRect"); + qRegisterMetaType<::QVector_QRectF>("QVector_QRectF"); + qRegisterMetaType<::QVector_QSize>("QVector_QSize"); + qRegisterMetaType<::QVector_QSizeF>("QVector_QSizeF"); + qRegisterMetaType<::QVector_QString>("QVector_QString"); + qRegisterMetaType<::QVector_QTime>("QVector_QTime"); + qRegisterMetaType<::QVector_QUrl>("QVector_QUrl"); + qRegisterMetaType<::QVector_QUuid>("QVector_QUuid"); + // Ensure that QVector (aka QVariantList) is registered + // otherwise it cannot be used in QML + qRegisterMetaType<::QVector_QVariant>("QVector_QVariant"); + qRegisterMetaType<::QVector_u8>("QVector_u8"); + qRegisterMetaType<::QVector_u16>("QVector_u16"); + qRegisterMetaType<::QVector_u32>("QVector_u32"); + qRegisterMetaType<::QVector_u64>("QVector_u64"); + }); + + return true; +} diff --git a/crates/cxx-qt-lib/src/gui/init.cpp b/crates/cxx-qt-lib/src/gui/init.cpp index 538edf5c1..907517037 100644 --- a/crates/cxx-qt-lib/src/gui/init.cpp +++ b/crates/cxx-qt-lib/src/gui/init.cpp @@ -8,7 +8,15 @@ #include #include -static const int register_QList_QColor = - qRegisterMetaType<::QList_QColor>("QList_QColor"); -static const int register_QVector_QColor = - qRegisterMetaType<::QVector_QColor>("QVector_QColor"); +#include + +extern "C" bool +init_cxx_qt_lib_gui() +{ + static std::once_flag flag; + std::call_once(flag, []() { + qRegisterMetaType<::QList_QColor>("QList_QColor"); + qRegisterMetaType<::QVector_QColor>("QVector_QColor"); + }); + return true; +} diff --git a/crates/cxx-qt/build.rs b/crates/cxx-qt/build.rs index efe12c08d..21176344f 100644 --- a/crates/cxx-qt/build.rs +++ b/crates/cxx-qt/build.rs @@ -52,6 +52,10 @@ fn main() { println!("cargo::rerun-if-changed={cpp_file}"); } }); + builder = builder.initializer(qt_build_utils::Initializer { + file: Some("src/init.cpp".into()), + ..qt_build_utils::Initializer::default_signature("init_cxx_qt_core") + }); builder.build(); } diff --git a/crates/cxx-qt/src/init.cpp b/crates/cxx-qt/src/init.cpp new file mode 100644 index 000000000..9571b84f9 --- /dev/null +++ b/crates/cxx-qt/src/init.cpp @@ -0,0 +1,50 @@ +// clang-format off +// SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company +// clang-format on +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +#include + +// For versions less than Qt 6 we need to manually register the std numerics +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) +#include + +#include +#include + +extern "C" bool +init_cxx_qt_core() +{ + static std::once_flag flag; + std::call_once(flag, []() { + // If we are using Qt 5 then register std numbers as a type for use in QML. + // + // See also: + // https://github.com/rust-lang/rust/issues/108081 + // https://github.com/KDAB/cxx-qt/pull/598 + qRegisterMetaType<::std::int8_t>("::std::int8_t"); + qRegisterMetaType<::std::int16_t>("::std::int16_t"); + qRegisterMetaType<::std::int32_t>("::std::int32_t"); + qRegisterMetaType<::std::int64_t>("::std::int64_t"); + + qRegisterMetaType<::std::uint8_t>("::std::uint8_t"); + qRegisterMetaType<::std::uint16_t>("::std::uint16_t"); + qRegisterMetaType<::std::uint32_t>("::std::uint32_t"); + qRegisterMetaType<::std::uint64_t>("::std::uint64_t"); + }); + + return true; +} + +#else + +extern "C" bool +init_cxx_qt_core() +{ + // Only needed for Qt5 + return true; +} + +#endif diff --git a/crates/qt-build-utils/Cargo.toml b/crates/qt-build-utils/Cargo.toml index 751503e76..acfc569e2 100644 --- a/crates/qt-build-utils/Cargo.toml +++ b/crates/qt-build-utils/Cargo.toml @@ -15,6 +15,7 @@ rust-version.workspace = true [dependencies] cc.workspace = true +serde = { workspace = true, optional = true } versions = "6.3" thiserror.workspace = true @@ -30,6 +31,7 @@ thiserror.workspace = true # # When linking Qt dynamically, this makes no difference. link_qt_object_files = [] +serde = ["dep:serde"] [lints] workspace = true diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs index c6efff722..a8975b8ab 100644 --- a/crates/qt-build-utils/src/lib.rs +++ b/crates/qt-build-utils/src/lib.rs @@ -135,6 +135,26 @@ pub fn setup_linker() { } } +#[doc(hidden)] +#[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Initializer { + pub file: Option, + pub init_call: Option, + pub init_declaration: Option, +} + +impl Initializer { + #[doc(hidden)] + pub fn default_signature(name: &str) -> Self { + Self { + file: None, + init_call: Some(format!("{name}();")), + init_declaration: Some(format!("extern \"C\" bool {name}();")), + } + } +} + /// Paths to files generated by [QtBuild::moc] pub struct MocProducts { /// Generated C++ file @@ -183,8 +203,8 @@ pub struct QmlModuleRegistrationFiles { pub qmltyperegistrar: PathBuf, /// File with generated [QQmlEngineExtensionPlugin](https://doc.qt.io/qt-6/qqmlengineextensionplugin.html) that calls the function generated by qmltyperegistrar. pub plugin: PathBuf, - /// File that automatically registers the QQmlExtensionPlugin at startup. Must be linked with `+whole-archive`. - pub plugin_init: PathBuf, + /// Initializer that automatically registers the QQmlExtensionPlugin at startup. + pub plugin_init: Initializer, /// An optional include path that should be included pub include_path: Option, } @@ -942,7 +962,6 @@ prefer :/qt/qml/{qml_uri_dirs}/ // Generate QQmlEngineExtensionPlugin let qml_plugin_cpp_path = qml_plugin_dir.join(format!("{plugin_class_name}.cpp")); - let qml_plugin_init_path = qml_plugin_dir.join(format!("{plugin_class_name}_init.cpp")); let include_path; { let mut declarations = Vec::default(); @@ -1005,34 +1024,40 @@ public: // Pass the include directory of the moc file to the caller include_path = moc_product.cpp.parent().map(|path| path.to_path_buf()); - // Generate file to load static QQmlExtensionPlugin - std::fs::write( - &qml_plugin_init_path, - format!( + // Generate Initializer for static QQmlExtensionPlugin + let plugin_init = Initializer { + file: None, + init_call: None, + init_declaration: Some(format!( r#" #include Q_IMPORT_PLUGIN({plugin_class_name}); "# - ), - ) - .expect("Failed to write plugin initializer file"); - } + )), + }; - QmlModuleRegistrationFiles { - rcc: self.qrc(&qrc_path), - qmlcachegen: qmlcachegen_file_paths, - qmltyperegistrar: qmltyperegistrar_output_path, - plugin: qml_plugin_cpp_path, - plugin_init: qml_plugin_init_path, - include_path, + let rcc = self.qrc(&qrc_path); + QmlModuleRegistrationFiles { + // The rcc file is automatically initialized when importing the plugin. + // so we don't need to treat it like an initializer here. + rcc: rcc.file.unwrap(), + qmlcachegen: qmlcachegen_file_paths, + qmltyperegistrar: qmltyperegistrar_output_path, + plugin: qml_plugin_cpp_path, + plugin_init, + include_path, + } } } /// Run [rcc](https://doc.qt.io/qt-6/resources.html) on a .qrc file and save the output into [cargo's OUT_DIR](https://doc.rust-lang.org/cargo/reference/environment-variables.html). /// The path to the generated C++ file is returned, which can then be passed to [cc::Build::files](https://docs.rs/cc/latest/cc/struct.Build.html#method.file). - /// The compiled static library must be linked with [+whole-archive](https://doc.rust-lang.org/rustc/command-line-arguments.html#linking-modifiers-whole-archive) - /// or the linker will discard the generated static variables because they are not referenced from `main`. - pub fn qrc(&mut self, input_file: &impl AsRef) -> PathBuf { + /// This function also returns a String that contains the name of the resource initializer + /// function. + /// The build system must ensure that if the .cpp file is built into a static library, either + /// the `+whole-archive` flag is used, or the initializer function is called by the + /// application. + pub fn qrc(&mut self, input_file: &impl AsRef) -> Initializer { if self.rcc_executable.is_none() { self.rcc_executable = Some(self.get_qt_tool("rcc").expect("Could not find rcc")); } @@ -1047,6 +1072,11 @@ Q_IMPORT_PLUGIN({plugin_class_name}); "{}.cpp", input_path.file_name().unwrap().to_string_lossy(), )); + let name = input_path + .file_name() + .unwrap() + .to_string_lossy() + .replace('.', "_"); let cmd = Command::new(self.rcc_executable.as_ref().unwrap()) .args([ @@ -1054,7 +1084,7 @@ Q_IMPORT_PLUGIN({plugin_class_name}); "-o", output_path.to_str().unwrap(), "--name", - input_path.file_name().unwrap().to_str().unwrap(), + &name, ]) .output() .unwrap_or_else(|_| panic!("rcc failed for {}", input_path.display())); @@ -1067,7 +1097,22 @@ Q_IMPORT_PLUGIN({plugin_class_name}); ); } - output_path + let qt_6_5 = SemVer { + major: 6, + minor: 5, + ..SemVer::default() + }; + let init_header = if self.version >= qt_6_5 { + // With Qt6.5 the Q_INIT_RESOURCE macro is in the QtResource header + "QtCore/QtResource" + } else { + "QtCore/QDir" + }; + Initializer { + file: Some(output_path), + init_call: Some(format!("Q_INIT_RESOURCE({name});")), + init_declaration: Some(format!("#include <{init_header}>")), + } } /// Run [rcc](https://doc.qt.io/qt-6/resources.html) on a .qrc file and return the paths of the sources