diff --git a/CHANGELOG.md b/CHANGELOG.md index ce53a5239..eb573f3f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 +- 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!`) ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 7bb0de658..eda162582 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -445,6 +445,7 @@ dependencies = [ "cxx-qt", "cxx-qt-gen", "proc-macro2", + "quote", "syn", ] @@ -1158,6 +1159,18 @@ dependencies = [ "cxx-qt-lib", ] +[[package]] +name = "qml_multi_crates" +version = "0.1.0" +dependencies = [ + "cxx", + "cxx-qt", + "cxx-qt-build", + "cxx-qt-lib", + "sub1", + "sub2", +] + [[package]] name = "qt-build-utils" version = "0.7.0" @@ -1293,6 +1306,26 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "sub1" +version = "0.1.0" +dependencies = [ + "cxx", + "cxx-qt", + "cxx-qt-build", + "cxx-qt-lib", +] + +[[package]] +name = "sub2" +version = "0.1.0" +dependencies = [ + "cxx", + "cxx-qt", + "cxx-qt-build", + "cxx-qt-lib", +] + [[package]] name = "syn" version = "2.0.98" diff --git a/Cargo.toml b/Cargo.toml index 791b6d7e6..a9ff9628f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,9 @@ members = [ "examples/qml_features/rust", "examples/qml_minimal/rust", "examples/qml_basics", + "examples/qml_multi_crates/rust/main", + "examples/qml_multi_crates/rust/sub1", + "examples/qml_multi_crates/rust/sub2", "tests/basic_cxx_only/rust", "tests/basic_cxx_qt/rust", diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index cc98c6bd2..7475bdb69 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -27,6 +27,7 @@ SPDX-License-Identifier: MIT OR Apache-2.0 - [Shared types](./bridge/shared_types.md) - [Attributes](./bridge/attributes.md) - [Traits](./bridge/traits.md) +- [Common Issues](./common-issues.md) - [For Contributors: CXX-Qt Internals](./internals/index.md) - [Build System](./internals/build-system.md) - [Crate Organization](./internals/crate-organization.md) diff --git a/book/src/common-issues.md b/book/src/common-issues.md new file mode 100644 index 000000000..2ad020195 --- /dev/null +++ b/book/src/common-issues.md @@ -0,0 +1,79 @@ + +# Common Issues + +## Cargo Linker Error: Undefined reference to `cxx_qt_init_` + +CXX-Qt recreates Qt's resource initialization system within a mix of Cargo and CMake. + +This initialization system generates functions that are prefixed with `cxx_qt_init_crate` or `cxx_qt_init_qml_module`. + +When building with Cargo, under certain crate setups you may encounter errors that the linker cannot find these functions, e.g.: + +```shell += note: /.../out/cxx-qt-build/qml_module_com_kdab_cxx_qt_demo/call-initializers.cpp:2: + error: undefined reference to 'cxx_qt_init_qml_module_com_kdab_cxx_qt_demo' + /.../out/cxx-qt-build/initializers/crate_another_crate/call-initializers.cpp:2: + error: undefined reference to 'cxx_qt_init_crate_another_crate' + clang: error: linker command failed with exit code 1 (use -v to see invocation) + + = note: some `extern` functions couldn't be found; some native libraries may need to be installed or have their path specified + = note: use the `-l` flag to specify native libraries to link + = note: use the `cargo:rustc-link-lib` directive to specify the native libraries to link with Cargo (see https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-lib) +``` + +To fix this issue, you need to make sure of two things: + +### 1. Ensure dependencies are used + +If a dependency is not used by the target currently being built, the Rust toolchain will not link to it. +This is particularly common if a dependency provides a QML module creates types for use in QML that aren't actually needed by the Rust code of downstream crates. + +To fix this, force the Rust compiler to link to the crate by adding: + +```rust,ignore +extern crate another_crate; +``` + +(where another_crate is replaced by the name of the dependency that isn't otherwise used). + +### 2. Include the initializers in your code + +Next we need to ensure the initializers can be found by the linker. + +If you followed step 1, modern linkers like `mold` or `lld` should already be able to link everything correctly. +We encourage switching to such a linker if you're still using the (now deprecated) `ld.gold` on Linux. + +With older linkers, you can force initialization manually by calling the corresponding `init_` macros from the cxx_qt crate at startup. + +```rust,ignore +fn main() { + cxx_qt::init_crate!(another_crate); + cxx_qt::init_qml_module!("com.kdab.cxx_qt.demo"); +} +``` + +Note that you will have to do the same in tests and doc-tests: + +````rust,ignore +/// ``` +/// # cxx_qt::init_crate!(another_crate); +/// # cxx_qt::init_qml_module!(another_crate); +/// +/// X::do_something(); +/// ``` +struct X {} + +#[cfg(test)] +mod tests { + #[test] + fn initialize_eependencies() { + cxx_qt::init_crate!(another_crate); + cxx_qt::init_qml_module!("com.kdab.cxx_qt.demo"); + } +} +```` diff --git a/book/src/internals/build-system.md b/book/src/internals/build-system.md index 1ebc61637..b3dba6915 100644 --- a/book/src/internals/build-system.md +++ b/book/src/internals/build-system.md @@ -19,46 +19,17 @@ 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 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 We want to make the generated headers available, not just to CMake, but also within dependents in the cargo build chain (e.g. your crate will probably want to depend on the headers produced by cxx-qt-lib). For this we need to export them to a stable directory so that both CMake and Cargo can find them. -### (Optional) Integration with CMake +# (Optional) Integration with CMake Somehow, all of this should be compatible with both CMake, and Cargo-only builds. -## The plan (for now) +# The plan (for now) After many rounds of refactoring this, we believe that we need to be able to share data between build scripts for this to work halfway ergonomically. @@ -97,6 +68,37 @@ Next to the crates directory, there should be a `qml_modules` directory, which c Each module should include a `plugin_init.o`, `.qmltypes`, `qmldir`, and any other necessary files. +## Initializers with Cargo and CMake + +There are multiple ways to solve the issues presented by static initializers: + +- 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. + ## Integration with CMake Via the `CXXQT_EXPORT_DIR` environment variable CMake should be able to change the location of the "target" directory. diff --git a/crates/cxx-qt-build/src/dir.rs b/crates/cxx-qt-build/src/dir.rs index 71641468e..6890b202e 100644 --- a/crates/cxx-qt-build/src/dir.rs +++ b/crates/cxx-qt-build/src/dir.rs @@ -40,17 +40,38 @@ pub(crate) fn crate_target() -> PathBuf { target().join("crates").join(crate_name()) } -/// The target directory, namespaced by plugin +/// The target directory, namespaced by QML module pub(crate) fn module_target(module_uri: &str) -> PathBuf { - target() - .join("qml_modules") - .join(module_name_from_uri(module_uri)) + module_export(module_uri).unwrap_or_else(|| { + out() + .join("qml_modules") + .join(module_name_from_uri(module_uri)) + }) +} + +/// The export directory, namespaced by QML module +/// +/// In conctrast to the crate_export directory, this is `Some` for downstream dependencies as well. +/// This allows CMake to import QML modules from dependencies. +/// +/// TODO: This may conflict if two dependencies are building QML modules with the same name! +/// We should probably include a lockfile here to avoid this. +pub(crate) fn module_export(module_uri: &str) -> Option { + // In contrast to crate_export, we don't need to check for the specific crate here. + // QML modules should always be exported. + env::var("CXX_QT_EXPORT_DIR") + .ok() + .map(PathBuf::from) + .map(|dir| { + dir.join("qml_modules") + .join(module_name_from_uri(module_uri)) + }) } /// The target directory or another directory where we can write files that will be shared /// between crates. pub(crate) fn target() -> PathBuf { - if let Some(export) = export() { + if let Some(export) = crate_export() { return export; } @@ -59,7 +80,7 @@ pub(crate) fn target() -> PathBuf { /// The export directory, if one was specified through the environment. /// Note that this is not namspaced by crate. -pub(crate) fn export() -> Option { +pub(crate) fn crate_export() -> Option { // Make sure to synchronize the naming of these variables with CMake! let export_flag = format!("CXX_QT_EXPORT_CRATE_{}", crate_name()); // We only want to export this crate if it is the specific crate that CMake is looking for and @@ -87,8 +108,8 @@ pub(crate) fn out() -> PathBuf { env::var("OUT_DIR").unwrap().into() } -pub(crate) fn is_exporting() -> bool { - export().is_some() +pub(crate) fn is_exporting_crate() -> bool { + crate_export().is_some() } pub(crate) fn initializers(key: &str) -> PathBuf { diff --git a/crates/cxx-qt-build/src/lib.rs b/crates/cxx-qt-build/src/lib.rs index 5932a0245..fd5d35093 100644 --- a/crates/cxx-qt-build/src/lib.rs +++ b/crates/cxx-qt-build/src/lib.rs @@ -34,7 +34,6 @@ 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, @@ -723,8 +722,11 @@ impl CxxQtBuilder { } } - fn export_object_file(builder: &cc::Build, file_path: impl AsRef, export_path: PathBuf) { - let mut obj_builder = builder.clone(); + fn export_object_file( + mut obj_builder: cc::Build, + file_path: impl AsRef, + export_path: PathBuf, + ) { obj_builder.file(file_path.as_ref()); // We only expect a single file, so destructure the vec. @@ -756,13 +758,15 @@ impl CxxQtBuilder { fn build_qml_modules( &mut self, - init_builder: &cc::Build, 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 { + // Extract qml_modules out of self so we don't have to hold onto `self` for the duration of + // the loop. + let qml_modules: Vec<_> = self.qml_modules.drain(..).collect(); + for qml_module in qml_modules { dir::clean(dir::module_target(&qml_module.uri)) .expect("Failed to clean qml module export directory!"); @@ -885,15 +889,14 @@ impl CxxQtBuilder { 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, + self.build_initializers( &private_initializers, &public_initializer, - dir::module_target(&qml_module.uri).join("plugin_init.o"), + dir::module_export(&qml_module.uri).map(|dir| dir.join("plugin_init.o")), &module_init_key, ); - initializer_functions.push(public_initializer); + initializer_functions.push(public_initializer.strip_file()); } initializer_functions } @@ -955,16 +958,14 @@ extern "C" bool {init_fun}() {{ } fn build_initializers<'a>( - init_builder: &cc::Build, + &mut self, private_initializers: impl IntoIterator, public_initializer: &qt_build_utils::Initializer, - export_path: PathBuf, + export_path: Option, 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 + // Build the initializers themselves into the main library. + self.cc_builder .file( public_initializer .file @@ -977,6 +978,13 @@ extern "C" bool {init_fun}() {{ .filter_map(|initializer| initializer.file.as_ref()), ); + // Build the initializer call into a separate library to be linked with whole-archive. + // We can just use a plain cc::Build for this, as this doesn't use any non-standard + // features. + let mut init_call_builder = cc::Build::new(); + let includes: &[&str] = &[]; // <-- Needed for type annotations + Self::setup_cc_builder(&mut init_call_builder, includes); + let init_call = format!( "{declaration}\nstatic const bool do_init_{key} = {init_call}", declaration = public_initializer @@ -992,24 +1000,23 @@ extern "C" bool {init_fun}() {{ 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); + if let Some(export_path) = export_path { + Self::export_object_file(init_call_builder, init_file, export_path); } else { - init_lib.file(init_file); + // Link the call-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 initializer call with +whole-archive, and not the entire + // Rust static library, as the initializer is 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_call_builder + .file(init_file) + .link_lib_modifier("+whole-archive") + .compile(&format!("cxx-qt-call-init-{key}")); } - - // 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( @@ -1117,15 +1124,7 @@ extern "C" bool {init_fun}() {{ qtbuild.cargo_link_libraries(&mut self.cc_builder); Self::define_qt_version_cfg_variables(qtbuild.version()); - // Setup compilers - // Static QML plugin and Qt resource initializers need to be linked as their own separate - // object files 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 the little amount of code that needs to be built & linked this way. - let mut init_builder = cc::Build::new(); // Ensure that Qt modules and apple framework are linked and searched correctly - qtbuild.cargo_link_libraries(&mut init_builder); let mut include_paths = qtbuild.include_paths(); include_paths.push(header_root.clone()); // TODO: Some of the code generated by qmltyperegistrar doesn't add the include_prefix to @@ -1137,13 +1136,6 @@ extern "C" bool {init_fun}() {{ Self::setup_cc_builder(&mut self.cc_builder, &include_paths); - Self::setup_cc_builder(&mut init_builder, &include_paths); - // Note: From now on the init_builder is correctly configured. - // When building object files with this builder, we always need to copy it first. - // So remove `mut` to ensure that we can't accidentally change the configuration or add - // files. - let init_builder = init_builder; - // Generate files self.generate_cpp_files_from_cxxqt_bridges(&header_root, &self.include_prefix.clone()); @@ -1151,12 +1143,8 @@ extern "C" bool {init_fun}() {{ // Bridges for QML modules are handled separately because // the metatypes_json generated by moc needs to be passed to qmltyperegistrar - let module_initializers = self.build_qml_modules( - &init_builder, - &mut qtbuild, - &header_root, - &self.include_prefix.clone(), - ); + let module_initializers = + self.build_qml_modules(&mut qtbuild, &header_root, &self.include_prefix.clone()); let qrc_files = self.generate_cpp_from_qrc_files(&mut qtbuild); @@ -1164,16 +1152,21 @@ extern "C" bool {init_fun}() {{ let private_initializers = dependency_initializers .into_iter() .chain(qrc_files) + .chain(module_initializers) .chain(self.init_files.iter().cloned()) .collect::>(); let public_initializer = Self::generate_public_initializer(&private_initializers, &crate_init_key()); - Self::build_initializers( - &init_builder, + let export_path = if dir::is_exporting_crate() { + Some(dir::crate_target().join("initializers.o")) + } else { + None + }; + self.build_initializers( &private_initializers, &public_initializer, - dir::crate_target().join("initializers.o"), + export_path, &crate_init_key(), ); @@ -1186,17 +1179,7 @@ extern "C" bool {init_fun}() {{ 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(), + vec![public_initializer.strip_file()], ); } } diff --git a/crates/cxx-qt-lib/src/core/mod.rs b/crates/cxx-qt-lib/src/core/mod.rs index 4d8fa36e9..736ead232 100644 --- a/crates/cxx-qt-lib/src/core/mod.rs +++ b/crates/cxx-qt-lib/src/core/mod.rs @@ -133,7 +133,9 @@ mod ffi { /// # /// # } /// # -/// # fn main() {} +/// # fn main() { +/// # cxx_qt::init_crate!(cxx_qt_lib); +/// # } /// ``` /// /// See: diff --git a/crates/cxx-qt-macro/Cargo.toml b/crates/cxx-qt-macro/Cargo.toml index 5437522cd..244ea85a9 100644 --- a/crates/cxx-qt-macro/Cargo.toml +++ b/crates/cxx-qt-macro/Cargo.toml @@ -22,6 +22,7 @@ proc-macro = true cxx-qt-gen.workspace = true proc-macro2.workspace = true syn.workspace = true +quote.workspace = true [dev-dependencies] cxx.workspace = true diff --git a/crates/cxx-qt-macro/src/lib.rs b/crates/cxx-qt-macro/src/lib.rs index f2b8972db..474977a78 100644 --- a/crates/cxx-qt-macro/src/lib.rs +++ b/crates/cxx-qt-macro/src/lib.rs @@ -39,7 +39,9 @@ use cxx_qt_gen::{write_rust, GeneratedRustBlocks, Parser}; /// } /// /// # // Note that we need a fake main for doc tests to build -/// # fn main() {} +/// # fn main() { +/// # cxx_qt::init_crate!(cxx_qt); +/// # } /// ``` #[proc_macro_attribute] pub fn bridge(args: TokenStream, input: TokenStream) -> TokenStream { @@ -83,7 +85,9 @@ pub fn bridge(args: TokenStream, input: TokenStream) -> TokenStream { /// pub struct MyObjectRust; /// /// # // Note that we need a fake main for doc tests to build -/// # fn main() {} +/// # fn main() { +/// # cxx_qt::init_crate!(cxx_qt); +/// # } /// ``` /// /// You can also specify a custom base class by using `#[base = QStringListModel]`, you must then use CXX to add any includes needed. @@ -110,13 +114,45 @@ pub fn bridge(args: TokenStream, input: TokenStream) -> TokenStream { /// pub struct MyModelRust; /// /// # // Note that we need a fake main for doc tests to build -/// # fn main() {} +/// # fn main() { +/// # cxx_qt::init_crate!(cxx_qt); +/// # } /// ``` #[proc_macro_attribute] pub fn qobject(_args: TokenStream, _input: TokenStream) -> TokenStream { unreachable!("qobject should not be used as a macro by itself. Instead it should be used within a cxx_qt::bridge definition") } +/// Force a crate to be initialized +#[proc_macro] +pub fn init_crate(args: TokenStream) -> TokenStream { + let crate_name = syn::parse_macro_input!(args as syn::Ident); + let function_name = quote::format_ident!("cxx_qt_init_crate_{crate_name}"); + quote::quote! { + extern "C" { + fn #function_name() -> bool; + } + unsafe { #function_name(); } + } + .into() +} + +/// Force a QML module with the given URI to be initialized +#[proc_macro] +pub fn init_qml_module(args: TokenStream) -> TokenStream { + let module_uri = syn::parse_macro_input!(args as syn::LitStr); + let module_name = syn::Ident::new(&module_uri.value().replace('.', "_"), module_uri.span()); + + let function_name = quote::format_ident!("cxx_qt_init_qml_module_{module_name}"); + quote::quote! { + extern "C" { + fn #function_name() -> bool; + } + unsafe { #function_name(); } + } + .into() +} + // Take the module and C++ namespace and generate the rust code fn extract_and_generate(module: ItemMod) -> TokenStream { Parser::from(module) diff --git a/crates/cxx-qt/src/lib.rs b/crates/cxx-qt/src/lib.rs index 63f32061d..fc06d9c32 100644 --- a/crates/cxx-qt/src/lib.rs +++ b/crates/cxx-qt/src/lib.rs @@ -18,6 +18,8 @@ pub mod signalhandler; mod threading; pub use cxx_qt_macro::bridge; +pub use cxx_qt_macro::init_crate; +pub use cxx_qt_macro::init_qml_module; pub use cxx_qt_macro::qobject; pub use connection::{ConnectionType, QMetaObjectConnection}; @@ -182,7 +184,9 @@ pub trait Upcast {} /// } /// /// # // Note that we need a fake main function for doc tests to build. -/// # fn main() {} +/// # fn main() { +/// # cxx_qt::init_crate!(cxx_qt); +/// # } /// ``` /// /// # Pseudo Code for generated C++ Constructor diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs index a8975b8ab..bca04b71f 100644 --- a/crates/qt-build-utils/src/lib.rs +++ b/crates/qt-build-utils/src/lib.rs @@ -153,6 +153,15 @@ impl Initializer { init_declaration: Some(format!("extern \"C\" bool {name}();")), } } + + #[doc(hidden)] + // Strip the init files from the public initializers + // For downstream dependencies, it's often enough to just declare the init function and + // call it. + pub fn strip_file(mut self) -> Self { + self.file = None; + self + } } /// Paths to files generated by [QtBuild::moc] diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 7cec23b99..3d4270d47 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -9,6 +9,7 @@ # When using `cargo test` add_subdirectory(qml_features) add_subdirectory(qml_minimal) +add_subdirectory(qml_multi_crates) # TODO: get demo_threading working for wasm builds if(NOT BUILD_WASM) diff --git a/examples/qml_multi_crates/CMakeLists.txt b/examples/qml_multi_crates/CMakeLists.txt new file mode 100644 index 000000000..6847d92a9 --- /dev/null +++ b/examples/qml_multi_crates/CMakeLists.txt @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +# SPDX-FileContributor: Andrew Hayzen +# +# SPDX-License-Identifier: MIT OR Apache-2.0 + +cmake_minimum_required(VERSION 3.24) + +project(example_qml_multi_crates) + +# Rust always links against non-debug Windows runtime on *-msvc targets +# Note it is best to set this on the command line to ensure all targets are consistent +# https://github.com/corrosion-rs/corrosion/blob/master/doc/src/common_issues.md#linking-debug-cc-libraries-into-rust-fails-on-windows-msvc-targets +# https://github.com/rust-lang/rust/issues/39016 +if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL") +endif() + +if(BUILD_WASM) + # Ensure Rust build for the correct target + set(Rust_CARGO_TARGET wasm32-unknown-emscripten) + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_package(Threads REQUIRED) +endif() + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CXXQT_QTCOMPONENTS Core Gui Qml QuickControls2 QuickTest Test) +if(NOT BUILD_WASM) + set(CXXQT_QTCOMPONENTS ${CXXQT_QTCOMPONENTS} QmlImportScanner) +endif() + +if(NOT USE_QT5) + find_package(Qt6 COMPONENTS ${CXXQT_QTCOMPONENTS}) +endif() +if(NOT Qt6_FOUND) + find_package(Qt5 5.15 COMPONENTS ${CXXQT_QTCOMPONENTS} REQUIRED) +endif() + +find_package(CxxQt QUIET) +if(NOT CxxQt_FOUND) + include(FetchContent) + FetchContent_Declare( + CxxQt + GIT_REPOSITORY https://github.com/kdab/cxx-qt-cmake.git + GIT_TAG main + ) + + FetchContent_MakeAvailable(CxxQt) +endif() + +cxx_qt_import_crate(MANIFEST_PATH rust/main/Cargo.toml + CRATES qml_multi_crates + CRATE_TYPES staticlib + QT_MODULES Qt::Core Qt::Gui Qt::Qml Qt::QuickControls2 Qt::Network) + +cxx_qt_import_qml_module(qml_multi_crates_main + URI "com.kdab.cxx_qt.demo" + SOURCE_CRATE qml_multi_crates) + +cxx_qt_import_qml_module(qml_multi_crates_sub1 + URI "com.kdab.cxx_qt.demo.sub1" + SOURCE_CRATE qml_multi_crates) + +cxx_qt_import_qml_module(qml_multi_crates_sub2 + URI "com.kdab.cxx_qt.demo.sub2" + SOURCE_CRATE qml_multi_crates) + +# Define the executable with the C++ source +if(BUILD_WASM) + # Currently need to use qt_add_executable + # for WASM builds, otherwise there is no + # HTML output. + # + # TODO: Figure out how to configure such that + # we can use add_executable for WASM + qt_add_executable(example_qml_multi_crates cpp/main.cpp) +else() + add_executable(example_qml_multi_crates cpp/main.cpp) +endif() + +# Link to the qml module, which in turn links to the Rust qml_multi_crates library +target_link_libraries(example_qml_multi_crates PRIVATE Qt::Core Qt::Gui Qt::Qml qml_multi_crates_main qml_multi_crates_sub1 qml_multi_crates_sub2) + +# If we are using a statically linked Qt then we need to import any qml plugins +qt_import_qml_plugins(example_qml_multi_crates) diff --git a/examples/qml_multi_crates/cpp/main.cpp b/examples/qml_multi_crates/cpp/main.cpp new file mode 100644 index 000000000..e6613bb93 --- /dev/null +++ b/examples/qml_multi_crates/cpp/main.cpp @@ -0,0 +1,32 @@ +// clang-format off +// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +// clang-format on +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +#include +#include + +int +main(int argc, char* argv[]) +{ + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; + + const QUrl url( + QStringLiteral("qrc:/qt/qml/com/kdab/cxx_qt/demo/qml/main.qml")); + QObject::connect( + &engine, + &QQmlApplicationEngine::objectCreated, + &app, + [url](QObject* obj, const QUrl& objUrl) { + if (!obj && url == objUrl) + QCoreApplication::exit(-1); + }, + Qt::QueuedConnection); + + engine.load(url); + + return app.exec(); +} diff --git a/examples/qml_multi_crates/qml/main.qml b/examples/qml_multi_crates/qml/main.qml new file mode 100644 index 000000000..18301e50f --- /dev/null +++ b/examples/qml_multi_crates/qml/main.qml @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Window 2.12 + +import com.kdab.cxx_qt.demo 1.0 +import com.kdab.cxx_qt.demo.sub1 1.0 +import com.kdab.cxx_qt.demo.sub2 1.0 + +ApplicationWindow { + id: window + minimumHeight: 480 + minimumWidth: 640 + title: qsTr("CXX-Qt: Hello World") + visible: true + + MainObject { + id: main + } + + Sub1Object { + id: sub1 + } + + Sub2Object { + id: sub2 + } + + Column { + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + Label { + text: "Main: " + main.string + } + + Label { + text: "Sub1: " + sub1.string + } + + Label { + text: "Sub2: " + sub2.string + } + + Button { + text: "Increment Number" + + onClicked: { + main.increment(); + sub1.increment(); + sub2.increment(); + } + } + } +} diff --git a/examples/qml_multi_crates/rust/main/Cargo.toml b/examples/qml_multi_crates/rust/main/Cargo.toml new file mode 100644 index 000000000..9032870a0 --- /dev/null +++ b/examples/qml_multi_crates/rust/main/Cargo.toml @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +# SPDX-FileContributor: Andrew Hayzen +# +# SPDX-License-Identifier: MIT OR Apache-2.0 +[package] +name = "qml_multi_crates" +version = "0.1.0" +authors = ["Andrew Hayzen "] +edition = "2021" +license = "MIT OR Apache-2.0" + +[lib] +crate-type = ["staticlib", "lib"] + +[dependencies] +sub1 = { path = "../sub1" } +sub2 = { path = "../sub2" } + +cxx.workspace = true +cxx-qt.workspace = true +cxx-qt-lib = { workspace = true, features = [ "qt_full" ] } + +[build-dependencies] +cxx-qt-build.workspace = true diff --git a/examples/qml_multi_crates/rust/main/build.rs b/examples/qml_multi_crates/rust/main/build.rs new file mode 100644 index 000000000..c09ddcf80 --- /dev/null +++ b/examples/qml_multi_crates/rust/main/build.rs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use cxx_qt_build::{CxxQtBuilder, QmlModule}; + +fn main() { + CxxQtBuilder::new() + .qt_module("Network") + .qml_module(QmlModule { + uri: "com.kdab.cxx_qt.demo", + rust_files: &["src/main_object.rs"], + qml_files: &["../../qml/main.qml"], + ..Default::default() + }) + .build(); +} diff --git a/examples/qml_multi_crates/rust/main/src/lib.rs b/examples/qml_multi_crates/rust/main/src/lib.rs new file mode 100644 index 000000000..9133ec875 --- /dev/null +++ b/examples/qml_multi_crates/rust/main/src/lib.rs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +mod main_object; + +// Ensure the symbols from the rlib dependencies end up +// in the staticlib (if you use Rust symbols from these +// crates in this crate, you can skip these `extern crate` statements). +extern crate sub1; +extern crate sub2; diff --git a/examples/qml_multi_crates/rust/main/src/main.rs b/examples/qml_multi_crates/rust/main/src/main.rs new file mode 100644 index 000000000..8354191c3 --- /dev/null +++ b/examples/qml_multi_crates/rust/main/src/main.rs @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Leon Matthes +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +extern crate qml_multi_crates; + +use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QUrl}; + +fn main() { + cxx_qt::init_crate!(qml_multi_crates); + + // Create the application and engine + let mut app = QGuiApplication::new(); + let mut engine = QQmlApplicationEngine::new(); + + // Load the QML path into the engine + if let Some(engine) = engine.as_mut() { + engine.load(&QUrl::from("qrc:/qt/qml/com/kdab/cxx_qt/demo/qml/main.qml")); + } + + if let Some(engine) = engine.as_mut() { + // Listen to a signal from the QML Engine + engine + .as_qqmlengine() + .on_quit(|_| { + println!("QML Quit!"); + }) + .release(); + } + + // Start the app + if let Some(app) = app.as_mut() { + app.exec(); + } +} + +#[cfg(test)] +mod tests { + // In the test cfg there needs to be at least one test that calls the crate initialization. + // Otherwise linking will fail! + #[test] + fn init_dependencies() { + cxx_qt::init_crate!(qml_multi_crates); + cxx_qt::init_qml_module!("com.kdab.cxx_qt.demo"); + } +} diff --git a/examples/qml_multi_crates/rust/main/src/main_object.rs b/examples/qml_multi_crates/rust/main/src/main_object.rs new file mode 100644 index 000000000..9fc69cef5 --- /dev/null +++ b/examples/qml_multi_crates/rust/main/src/main_object.rs @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +#[cxx_qt::bridge] +pub mod qobject { + unsafe extern "C++" { + include!("cxx-qt-lib/qstring.h"); + type QString = cxx_qt_lib::QString; + } + + unsafe extern "RustQt" { + #[qobject] + #[qml_element] + #[qproperty(QString, string)] + type MainObject = super::MainObjectRust; + + #[qinvokable] + fn increment(self: Pin<&mut MainObject>); + } +} + +use core::pin::Pin; +use cxx_qt::CxxQtType; +use cxx_qt_lib::QString; + +#[derive(Default)] +pub struct MainObjectRust { + string: QString, + + pub counter: u32, +} + +impl qobject::MainObject { + pub fn increment(mut self: Pin<&mut Self>) { + self.as_mut().rust_mut().counter = self.rust().counter + 1; + + let new_string = QString::from(&self.rust().counter.to_string()); + self.as_mut().set_string(new_string); + } +} diff --git a/examples/qml_multi_crates/rust/sub1/Cargo.toml b/examples/qml_multi_crates/rust/sub1/Cargo.toml new file mode 100644 index 000000000..f38f96056 --- /dev/null +++ b/examples/qml_multi_crates/rust/sub1/Cargo.toml @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +# SPDX-FileContributor: Andrew Hayzen +# +# SPDX-License-Identifier: MIT OR Apache-2.0 +[package] +name = "sub1" +version = "0.1.0" +authors = [ + "Andrew Hayzen ", +] +edition = "2021" +license = "MIT OR Apache-2.0" + +links = "sub1" + +[dependencies] +cxx.workspace = true +cxx-qt.workspace = true +cxx-qt-lib.workspace = true + +[build-dependencies] +cxx-qt-build.workspace = true diff --git a/examples/qml_multi_crates/rust/sub1/build.rs b/examples/qml_multi_crates/rust/sub1/build.rs new file mode 100644 index 000000000..6652b1c0d --- /dev/null +++ b/examples/qml_multi_crates/rust/sub1/build.rs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use cxx_qt_build::{CxxQtBuilder, Interface, QmlModule}; + +fn main() { + let interface = Interface::default(); + CxxQtBuilder::library(interface) + .qt_module("Network") + .qml_module(QmlModule::<_, &str> { + uri: "com.kdab.cxx_qt.demo.sub1", + rust_files: &["src/sub1_object.rs"], + ..Default::default() + }) + .build(); +} diff --git a/examples/qml_multi_crates/rust/sub1/src/lib.rs b/examples/qml_multi_crates/rust/sub1/src/lib.rs new file mode 100644 index 000000000..eaedc0171 --- /dev/null +++ b/examples/qml_multi_crates/rust/sub1/src/lib.rs @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +// We need to enable packed bundled libs to allow for +bundle and +whole-archive +// https://github.com/rust-lang/rust/issues/108081 + +mod sub1_object; + +pub fn increment(number: u32) -> u32 { + number + 2 +} diff --git a/examples/qml_multi_crates/rust/sub1/src/sub1_object.rs b/examples/qml_multi_crates/rust/sub1/src/sub1_object.rs new file mode 100644 index 000000000..f2bdf0679 --- /dev/null +++ b/examples/qml_multi_crates/rust/sub1/src/sub1_object.rs @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +#[cxx_qt::bridge] +pub mod qobject { + unsafe extern "C++" { + include!("cxx-qt-lib/qstring.h"); + type QString = cxx_qt_lib::QString; + } + + unsafe extern "RustQt" { + #[qobject] + #[qml_element] + #[qproperty(QString, string)] + type Sub1Object = super::Sub1ObjectRust; + + #[qinvokable] + fn increment(self: Pin<&mut Sub1Object>); + } +} + +use core::pin::Pin; +use cxx_qt::CxxQtType; +use cxx_qt_lib::QString; + +#[derive(Default)] +pub struct Sub1ObjectRust { + string: QString, + + pub counter: u32, +} + +impl qobject::Sub1Object { + pub fn increment(mut self: Pin<&mut Self>) { + self.as_mut().rust_mut().counter = crate::increment(self.rust().counter); + + let new_string = QString::from(&self.rust().counter.to_string()); + self.as_mut().set_string(new_string); + } +} diff --git a/examples/qml_multi_crates/rust/sub2/Cargo.toml b/examples/qml_multi_crates/rust/sub2/Cargo.toml new file mode 100644 index 000000000..97f0fbac4 --- /dev/null +++ b/examples/qml_multi_crates/rust/sub2/Cargo.toml @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +# SPDX-FileContributor: Andrew Hayzen +# +# SPDX-License-Identifier: MIT OR Apache-2.0 +[package] +name = "sub2" +version = "0.1.0" +authors = [ + "Andrew Hayzen ", +] +edition = "2021" +license = "MIT OR Apache-2.0" + +links = "sub2" + +[dependencies] +cxx.workspace = true +cxx-qt.workspace = true +cxx-qt-lib.workspace = true + +[build-dependencies] +cxx-qt-build.workspace = true diff --git a/examples/qml_multi_crates/rust/sub2/build.rs b/examples/qml_multi_crates/rust/sub2/build.rs new file mode 100644 index 000000000..cdbb1c9a2 --- /dev/null +++ b/examples/qml_multi_crates/rust/sub2/build.rs @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use cxx_qt_build::{CxxQtBuilder, Interface, QmlModule}; + +fn main() { + let interface = Interface::default(); + CxxQtBuilder::library(interface) + .qml_module(QmlModule::<_, &str> { + uri: "com.kdab.cxx_qt.demo.sub2", + rust_files: &["src/sub2_object.rs"], + ..Default::default() + }) + .build(); +} diff --git a/examples/qml_multi_crates/rust/sub2/src/lib.rs b/examples/qml_multi_crates/rust/sub2/src/lib.rs new file mode 100644 index 000000000..689f1ece5 --- /dev/null +++ b/examples/qml_multi_crates/rust/sub2/src/lib.rs @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +// We need to enable packed bundled libs to allow for +bundle and +whole-archive +// https://github.com/rust-lang/rust/issues/108081 + +mod sub2_object; + +pub fn increment(number: u32) -> u32 { + number + 3 +} diff --git a/examples/qml_multi_crates/rust/sub2/src/sub2_object.rs b/examples/qml_multi_crates/rust/sub2/src/sub2_object.rs new file mode 100644 index 000000000..f520f235f --- /dev/null +++ b/examples/qml_multi_crates/rust/sub2/src/sub2_object.rs @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +#[cxx_qt::bridge] +pub mod qobject { + unsafe extern "C++" { + include!("cxx-qt-lib/qstring.h"); + type QString = cxx_qt_lib::QString; + } + + unsafe extern "RustQt" { + #[qobject] + #[qml_element] + #[qproperty(QString, string)] + type Sub2Object = super::Sub2ObjectRust; + + #[qinvokable] + fn increment(self: Pin<&mut Sub2Object>); + } +} + +use core::pin::Pin; +use cxx_qt::CxxQtType; +use cxx_qt_lib::QString; + +#[derive(Default)] +pub struct Sub2ObjectRust { + string: QString, + + pub counter: u32, +} + +impl qobject::Sub2Object { + pub fn increment(mut self: Pin<&mut Self>) { + self.as_mut().rust_mut().counter = crate::increment(self.rust().counter); + + let new_string = QString::from(&self.rust().counter.to_string()); + self.as_mut().set_string(new_string); + } +}