From 286c26369c66a2773581ad72cd635ee40e247dfd Mon Sep 17 00:00:00 2001 From: Andrew Hayzen Date: Wed, 28 Jun 2023 18:38:40 +0100 Subject: [PATCH 1/9] examples: Multiple crate project --- Cargo.toml | 3 + examples/CMakeLists.txt | 1 + examples/meta_project/CMakeLists.txt | 86 +++++++++++++++++++ examples/meta_project/cpp/main.cpp | 32 +++++++ examples/meta_project/qml/main.qml | 60 +++++++++++++ examples/meta_project/rust/main/Cargo.toml | 24 ++++++ examples/meta_project/rust/main/build.rs | 18 ++++ examples/meta_project/rust/main/src/lib.rs | 12 +++ .../meta_project/rust/main/src/main_object.rs | 42 +++++++++ examples/meta_project/rust/sub1/Cargo.toml | 22 +++++ examples/meta_project/rust/sub1/build.rs | 18 ++++ examples/meta_project/rust/sub1/src/lib.rs | 13 +++ .../meta_project/rust/sub1/src/sub1_object.rs | 42 +++++++++ examples/meta_project/rust/sub2/Cargo.toml | 22 +++++ examples/meta_project/rust/sub2/build.rs | 17 ++++ examples/meta_project/rust/sub2/src/lib.rs | 13 +++ .../meta_project/rust/sub2/src/sub2_object.rs | 42 +++++++++ 17 files changed, 467 insertions(+) create mode 100644 examples/meta_project/CMakeLists.txt create mode 100644 examples/meta_project/cpp/main.cpp create mode 100644 examples/meta_project/qml/main.qml create mode 100644 examples/meta_project/rust/main/Cargo.toml create mode 100644 examples/meta_project/rust/main/build.rs create mode 100644 examples/meta_project/rust/main/src/lib.rs create mode 100644 examples/meta_project/rust/main/src/main_object.rs create mode 100644 examples/meta_project/rust/sub1/Cargo.toml create mode 100644 examples/meta_project/rust/sub1/build.rs create mode 100644 examples/meta_project/rust/sub1/src/lib.rs create mode 100644 examples/meta_project/rust/sub1/src/sub1_object.rs create mode 100644 examples/meta_project/rust/sub2/Cargo.toml create mode 100644 examples/meta_project/rust/sub2/build.rs create mode 100644 examples/meta_project/rust/sub2/src/lib.rs create mode 100644 examples/meta_project/rust/sub2/src/sub2_object.rs diff --git a/Cargo.toml b/Cargo.toml index 791b6d7e6..eaccb355b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,9 @@ members = [ "examples/qml_features/rust", "examples/qml_minimal/rust", "examples/qml_basics", + "examples/meta_project/rust/main", + "examples/meta_project/rust/sub1", + "examples/meta_project/rust/sub2", "tests/basic_cxx_only/rust", "tests/basic_cxx_qt/rust", diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 7cec23b99..f7dc8f19c 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(meta_project) # TODO: get demo_threading working for wasm builds if(NOT BUILD_WASM) diff --git a/examples/meta_project/CMakeLists.txt b/examples/meta_project/CMakeLists.txt new file mode 100644 index 000000000..516fa532d --- /dev/null +++ b/examples/meta_project/CMakeLists.txt @@ -0,0 +1,86 @@ +# 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_meta_project) + +# 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_meta_project) +target_link_libraries(qml_meta_project INTERFACE Qt::Core Qt::Gui Qt::Qml Qt::QuickControls2) + +cxx_qt_import_qml_module(qml_meta_project_main + URI "com.kdab.cxx_qt.demo" + SOURCE_CRATE qml_meta_project) + +cxx_qt_import_qml_module(qml_meta_project_sub1 + URI "com.kdab.cxx_qt.demo.sub1" + SOURCE_CRATE qml_meta_project) + +cxx_qt_import_qml_module(qml_meta_project_sub2 + URI "com.kdab.cxx_qt.demo.sub2" + SOURCE_CRATE qml_meta_project) + +# 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_meta_project cpp/main.cpp) +else() + add_executable(example_meta_project cpp/main.cpp) +endif() + +# Link to the qml module, which in turn links to the Rust qml_meta_project library +target_link_libraries(example_meta_project PRIVATE Qt::Core Qt::Gui Qt::Qml qml_meta_project_main qml_meta_project_sub1 qml_meta_project_sub2) + +# If we are using a statically linked Qt then we need to import any qml plugins +qt_import_qml_plugins(example_meta_project) diff --git a/examples/meta_project/cpp/main.cpp b/examples/meta_project/cpp/main.cpp new file mode 100644 index 000000000..e6613bb93 --- /dev/null +++ b/examples/meta_project/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/meta_project/qml/main.qml b/examples/meta_project/qml/main.qml new file mode 100644 index 000000000..18301e50f --- /dev/null +++ b/examples/meta_project/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/meta_project/rust/main/Cargo.toml b/examples/meta_project/rust/main/Cargo.toml new file mode 100644 index 000000000..de1082d84 --- /dev/null +++ b/examples/meta_project/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_meta_project" +version = "0.1.0" +authors = ["Andrew Hayzen "] +edition = "2021" +license = "MIT OR Apache-2.0" + +[lib] +crate-type = ["staticlib"] + +[dependencies] +sub1 = { path = "../sub1" } +sub2 = { path = "../sub2" } + +cxx.workspace = true +cxx-qt.workspace = true +cxx-qt-lib.workspace = true + +[build-dependencies] +cxx-qt-build.workspace = true diff --git a/examples/meta_project/rust/main/build.rs b/examples/meta_project/rust/main/build.rs new file mode 100644 index 000000000..194054c84 --- /dev/null +++ b/examples/meta_project/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::<_, &str> { + uri: "com.kdab.cxx_qt.demo", + rust_files: &["src/main_object.rs"], + qml_files: &["../../qml/main.qml"], + ..Default::default() + }) + .build(); +} diff --git a/examples/meta_project/rust/main/src/lib.rs b/examples/meta_project/rust/main/src/lib.rs new file mode 100644 index 000000000..73b514826 --- /dev/null +++ b/examples/meta_project/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 `pub use` statements). +pub use sub1; +pub use sub2; diff --git a/examples/meta_project/rust/main/src/main_object.rs b/examples/meta_project/rust/main/src/main_object.rs new file mode 100644 index 000000000..9fc69cef5 --- /dev/null +++ b/examples/meta_project/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/meta_project/rust/sub1/Cargo.toml b/examples/meta_project/rust/sub1/Cargo.toml new file mode 100644 index 000000000..f38f96056 --- /dev/null +++ b/examples/meta_project/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/meta_project/rust/sub1/build.rs b/examples/meta_project/rust/sub1/build.rs new file mode 100644 index 000000000..6652b1c0d --- /dev/null +++ b/examples/meta_project/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/meta_project/rust/sub1/src/lib.rs b/examples/meta_project/rust/sub1/src/lib.rs new file mode 100644 index 000000000..eaedc0171 --- /dev/null +++ b/examples/meta_project/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/meta_project/rust/sub1/src/sub1_object.rs b/examples/meta_project/rust/sub1/src/sub1_object.rs new file mode 100644 index 000000000..f2bdf0679 --- /dev/null +++ b/examples/meta_project/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/meta_project/rust/sub2/Cargo.toml b/examples/meta_project/rust/sub2/Cargo.toml new file mode 100644 index 000000000..97f0fbac4 --- /dev/null +++ b/examples/meta_project/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/meta_project/rust/sub2/build.rs b/examples/meta_project/rust/sub2/build.rs new file mode 100644 index 000000000..cdbb1c9a2 --- /dev/null +++ b/examples/meta_project/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/meta_project/rust/sub2/src/lib.rs b/examples/meta_project/rust/sub2/src/lib.rs new file mode 100644 index 000000000..689f1ece5 --- /dev/null +++ b/examples/meta_project/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/meta_project/rust/sub2/src/sub2_object.rs b/examples/meta_project/rust/sub2/src/sub2_object.rs new file mode 100644 index 000000000..f520f235f --- /dev/null +++ b/examples/meta_project/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); + } +} From 62715ba1ae920c4e25c608b6971f1203cc71e95f Mon Sep 17 00:00:00 2001 From: Be Wilson Date: Wed, 29 Nov 2023 02:03:16 -0600 Subject: [PATCH 2/9] meta_project: specify and document Rust 1.74 MSRV This isn't required for the libraries published to crates.io, so this isn't specified in the workspace Cargo.toml. --- examples/meta_project/rust/main/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/meta_project/rust/main/Cargo.toml b/examples/meta_project/rust/main/Cargo.toml index de1082d84..c2d9504e2 100644 --- a/examples/meta_project/rust/main/Cargo.toml +++ b/examples/meta_project/rust/main/Cargo.toml @@ -8,6 +8,10 @@ version = "0.1.0" authors = ["Andrew Hayzen "] edition = "2021" license = "MIT OR Apache-2.0" +# Linking CXX-Qt crates as rlibs requires a compiler feature +# that was stabilized in Rust 1.74, +# combining +whole-archive and +bundle link modifiers: https://github.com/rust-lang/rust/pull/113301 +rust-version = "1.74" [lib] crate-type = ["staticlib"] From 51c48e77b5b218b8862683ba9605250ddcefe7e5 Mon Sep 17 00:00:00 2001 From: Be Wilson Date: Fri, 1 Dec 2023 02:30:51 -0600 Subject: [PATCH 3/9] meta_project: use `extern crate` rather than `pub use` No need to change the privacy of these; just need to have the symbols referenced within the staticlib crate. --- examples/meta_project/rust/main/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/meta_project/rust/main/src/lib.rs b/examples/meta_project/rust/main/src/lib.rs index 73b514826..9133ec875 100644 --- a/examples/meta_project/rust/main/src/lib.rs +++ b/examples/meta_project/rust/main/src/lib.rs @@ -7,6 +7,6 @@ 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 `pub use` statements). -pub use sub1; -pub use sub2; +// crates in this crate, you can skip these `extern crate` statements). +extern crate sub1; +extern crate sub2; From 57f5df08abb41fab8f71519f6883c4c3a141cfd7 Mon Sep 17 00:00:00 2001 From: Leon Matthes Date: Wed, 5 Feb 2025 20:38:01 +0100 Subject: [PATCH 4/9] Allow multi-crate projects with new build system This also adds cxx_qt::init_crate! and init_qml_module! --- crates/cxx-qt-build/src/lib.rs | 9 +++- crates/cxx-qt-lib/src/core/mod.rs | 4 +- crates/cxx-qt-macro/Cargo.toml | 1 + crates/cxx-qt-macro/src/lib.rs | 42 ++++++++++++++++-- crates/cxx-qt/src/lib.rs | 6 ++- examples/CMakeLists.txt | 2 +- examples/meta_project/rust/main/Cargo.toml | 10 ++--- examples/meta_project/rust/main/build.rs | 2 +- examples/meta_project/rust/main/src/main.rs | 48 +++++++++++++++++++++ 9 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 examples/meta_project/rust/main/src/main.rs diff --git a/crates/cxx-qt-build/src/lib.rs b/crates/cxx-qt-build/src/lib.rs index 5932a0245..7f855b827 100644 --- a/crates/cxx-qt-build/src/lib.rs +++ b/crates/cxx-qt-build/src/lib.rs @@ -962,6 +962,7 @@ extern "C" bool {init_fun}() {{ key: &str, ) { let mut init_lib = init_builder.clone(); + let mut init_call_lib = init_builder.clone(); // Build static initializers into their own library which will be linked with whole-archive. init_lib @@ -995,7 +996,10 @@ extern "C" bool {init_fun}() {{ if dir::is_exporting() { Self::export_object_file(init_builder, init_file, export_path); } else { - init_lib.file(init_file); + init_call_lib + .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. @@ -1008,7 +1012,8 @@ extern "C" bool {init_fun}() {{ // 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") + // .link_lib_modifier("+whole-archive,+bundle") + // .link_lib_modifier("+bundle") .compile(&format!("cxx-qt-init-lib-{}", key)); } 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/examples/CMakeLists.txt b/examples/CMakeLists.txt index f7dc8f19c..c92ffe6a0 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -9,7 +9,7 @@ # When using `cargo test` add_subdirectory(qml_features) add_subdirectory(qml_minimal) -add_subdirectory(meta_project) +# add_subdirectory(meta_project) # TODO: get demo_threading working for wasm builds if(NOT BUILD_WASM) diff --git a/examples/meta_project/rust/main/Cargo.toml b/examples/meta_project/rust/main/Cargo.toml index c2d9504e2..bfef3cd8f 100644 --- a/examples/meta_project/rust/main/Cargo.toml +++ b/examples/meta_project/rust/main/Cargo.toml @@ -8,13 +8,9 @@ version = "0.1.0" authors = ["Andrew Hayzen "] edition = "2021" license = "MIT OR Apache-2.0" -# Linking CXX-Qt crates as rlibs requires a compiler feature -# that was stabilized in Rust 1.74, -# combining +whole-archive and +bundle link modifiers: https://github.com/rust-lang/rust/pull/113301 -rust-version = "1.74" -[lib] -crate-type = ["staticlib"] +# [lib] +# crate-type = ["staticlib", "lib"] [dependencies] sub1 = { path = "../sub1" } @@ -22,7 +18,7 @@ sub2 = { path = "../sub2" } cxx.workspace = true cxx-qt.workspace = true -cxx-qt-lib.workspace = true +cxx-qt-lib = { workspace = true, features = [ "qt_full" ] } [build-dependencies] cxx-qt-build.workspace = true diff --git a/examples/meta_project/rust/main/build.rs b/examples/meta_project/rust/main/build.rs index 194054c84..c09ddcf80 100644 --- a/examples/meta_project/rust/main/build.rs +++ b/examples/meta_project/rust/main/build.rs @@ -8,7 +8,7 @@ use cxx_qt_build::{CxxQtBuilder, QmlModule}; fn main() { CxxQtBuilder::new() .qt_module("Network") - .qml_module(QmlModule::<_, &str> { + .qml_module(QmlModule { uri: "com.kdab.cxx_qt.demo", rust_files: &["src/main_object.rs"], qml_files: &["../../qml/main.qml"], diff --git a/examples/meta_project/rust/main/src/main.rs b/examples/meta_project/rust/main/src/main.rs new file mode 100644 index 000000000..2281f609b --- /dev/null +++ b/examples/meta_project/rust/main/src/main.rs @@ -0,0 +1,48 @@ +// 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_meta_project; + +use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QUrl}; + +fn main() { + cxx_qt::init_crate!(qml_meta_project); + cxx_qt::init_qml_module!("com.kdab.cxx_qt.demo"); + + // 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_meta_project); + cxx_qt::init_qml_module!("com.kdab.cxx_qt.demo"); + } +} From 5a22a025b8099ce56040606f568ce62ac604c96d Mon Sep 17 00:00:00 2001 From: Leon Matthes Date: Thu, 6 Feb 2025 16:22:21 +0100 Subject: [PATCH 5/9] Update book with Common Issues section --- book/src/SUMMARY.md | 1 + book/src/common-issues.md | 79 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 book/src/common-issues.md 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"); + } +} +```` From 3dcd746842c148b957b67de74159dec230fb252f Mon Sep 17 00:00:00 2001 From: Leon Matthes Date: Thu, 6 Feb 2025 16:32:28 +0100 Subject: [PATCH 6/9] Improve CMake compatibility with multiple crates Previously, downstream QML modules weren't exported. This should now be fixed, as QML modules are always exported. --- crates/cxx-qt-build/src/dir.rs | 37 ++++++++++--- crates/cxx-qt-build/src/lib.rs | 98 +++++++++++++++------------------- 2 files changed, 72 insertions(+), 63 deletions(-) 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 7f855b827..8f7d1a600 100644 --- a/crates/cxx-qt-build/src/lib.rs +++ b/crates/cxx-qt-build/src/lib.rs @@ -723,8 +723,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 +759,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,11 +890,10 @@ 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, ); @@ -955,17 +959,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(); - let mut init_call_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 @@ -978,6 +979,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 @@ -993,28 +1001,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_call_lib + // 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,+bundle") - // .link_lib_modifier("+bundle") - .compile(&format!("cxx-qt-init-lib-{}", key)); } fn generate_cpp_from_qrc_files( @@ -1122,15 +1125,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 @@ -1142,13 +1137,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()); @@ -1156,12 +1144,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); @@ -1174,11 +1158,15 @@ extern "C" bool {init_fun}() {{ 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(), ); From 45663338e5ff7b410703f12421a7426cffb190df Mon Sep 17 00:00:00 2001 From: Leon Matthes Date: Thu, 6 Feb 2025 16:33:55 +0100 Subject: [PATCH 7/9] meta_project -> qml_multi_crates & enable CMake qml_multi_crates is the grand finale in our quest for CMake&Cargo compatibility. It is a project that can be built as a Cargo lib and binary, as well as imported into CMake with a C++ binary and uses multiple Cargo crates internally. It does rely on the init_crate! and init_qml_module! macros, but otherwise works quite seamless. --- Cargo.toml | 6 ++-- examples/CMakeLists.txt | 2 +- .../CMakeLists.txt | 30 ++++++++++--------- .../cpp/main.cpp | 0 .../qml/main.qml | 0 .../rust/main/Cargo.toml | 6 ++-- .../rust/main/build.rs | 0 .../rust/main/src/lib.rs | 0 .../rust/main/src/main.rs | 6 ++-- .../rust/main/src/main_object.rs | 0 .../rust/sub1/Cargo.toml | 0 .../rust/sub1/build.rs | 0 .../rust/sub1/src/lib.rs | 0 .../rust/sub1/src/sub1_object.rs | 0 .../rust/sub2/Cargo.toml | 0 .../rust/sub2/build.rs | 0 .../rust/sub2/src/lib.rs | 0 .../rust/sub2/src/sub2_object.rs | 0 18 files changed, 26 insertions(+), 24 deletions(-) rename examples/{meta_project => qml_multi_crates}/CMakeLists.txt (70%) rename examples/{meta_project => qml_multi_crates}/cpp/main.cpp (100%) rename examples/{meta_project => qml_multi_crates}/qml/main.qml (100%) rename examples/{meta_project => qml_multi_crates}/rust/main/Cargo.toml (89%) rename examples/{meta_project => qml_multi_crates}/rust/main/build.rs (100%) rename examples/{meta_project => qml_multi_crates}/rust/main/src/lib.rs (100%) rename examples/{meta_project => qml_multi_crates}/rust/main/src/main.rs (91%) rename examples/{meta_project => qml_multi_crates}/rust/main/src/main_object.rs (100%) rename examples/{meta_project => qml_multi_crates}/rust/sub1/Cargo.toml (100%) rename examples/{meta_project => qml_multi_crates}/rust/sub1/build.rs (100%) rename examples/{meta_project => qml_multi_crates}/rust/sub1/src/lib.rs (100%) rename examples/{meta_project => qml_multi_crates}/rust/sub1/src/sub1_object.rs (100%) rename examples/{meta_project => qml_multi_crates}/rust/sub2/Cargo.toml (100%) rename examples/{meta_project => qml_multi_crates}/rust/sub2/build.rs (100%) rename examples/{meta_project => qml_multi_crates}/rust/sub2/src/lib.rs (100%) rename examples/{meta_project => qml_multi_crates}/rust/sub2/src/sub2_object.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index eaccb355b..a9ff9628f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,9 +18,9 @@ members = [ "examples/qml_features/rust", "examples/qml_minimal/rust", "examples/qml_basics", - "examples/meta_project/rust/main", - "examples/meta_project/rust/sub1", - "examples/meta_project/rust/sub2", + "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/examples/CMakeLists.txt b/examples/CMakeLists.txt index c92ffe6a0..3d4270d47 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -9,7 +9,7 @@ # When using `cargo test` add_subdirectory(qml_features) add_subdirectory(qml_minimal) -# add_subdirectory(meta_project) +add_subdirectory(qml_multi_crates) # TODO: get demo_threading working for wasm builds if(NOT BUILD_WASM) diff --git a/examples/meta_project/CMakeLists.txt b/examples/qml_multi_crates/CMakeLists.txt similarity index 70% rename from examples/meta_project/CMakeLists.txt rename to examples/qml_multi_crates/CMakeLists.txt index 516fa532d..6847d92a9 100644 --- a/examples/meta_project/CMakeLists.txt +++ b/examples/qml_multi_crates/CMakeLists.txt @@ -5,7 +5,7 @@ cmake_minimum_required(VERSION 3.24) -project(example_meta_project) +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 @@ -51,20 +51,22 @@ if(NOT CxxQt_FOUND) FetchContent_MakeAvailable(CxxQt) endif() -cxx_qt_import_crate(MANIFEST_PATH rust/main/Cargo.toml CRATES qml_meta_project) -target_link_libraries(qml_meta_project INTERFACE Qt::Core Qt::Gui Qt::Qml Qt::QuickControls2) +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_meta_project_main +cxx_qt_import_qml_module(qml_multi_crates_main URI "com.kdab.cxx_qt.demo" - SOURCE_CRATE qml_meta_project) + SOURCE_CRATE qml_multi_crates) -cxx_qt_import_qml_module(qml_meta_project_sub1 +cxx_qt_import_qml_module(qml_multi_crates_sub1 URI "com.kdab.cxx_qt.demo.sub1" - SOURCE_CRATE qml_meta_project) + SOURCE_CRATE qml_multi_crates) -cxx_qt_import_qml_module(qml_meta_project_sub2 +cxx_qt_import_qml_module(qml_multi_crates_sub2 URI "com.kdab.cxx_qt.demo.sub2" - SOURCE_CRATE qml_meta_project) + SOURCE_CRATE qml_multi_crates) # Define the executable with the C++ source if(BUILD_WASM) @@ -74,13 +76,13 @@ if(BUILD_WASM) # # TODO: Figure out how to configure such that # we can use add_executable for WASM - qt_add_executable(example_meta_project cpp/main.cpp) + qt_add_executable(example_qml_multi_crates cpp/main.cpp) else() - add_executable(example_meta_project cpp/main.cpp) + add_executable(example_qml_multi_crates cpp/main.cpp) endif() -# Link to the qml module, which in turn links to the Rust qml_meta_project library -target_link_libraries(example_meta_project PRIVATE Qt::Core Qt::Gui Qt::Qml qml_meta_project_main qml_meta_project_sub1 qml_meta_project_sub2) +# 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_meta_project) +qt_import_qml_plugins(example_qml_multi_crates) diff --git a/examples/meta_project/cpp/main.cpp b/examples/qml_multi_crates/cpp/main.cpp similarity index 100% rename from examples/meta_project/cpp/main.cpp rename to examples/qml_multi_crates/cpp/main.cpp diff --git a/examples/meta_project/qml/main.qml b/examples/qml_multi_crates/qml/main.qml similarity index 100% rename from examples/meta_project/qml/main.qml rename to examples/qml_multi_crates/qml/main.qml diff --git a/examples/meta_project/rust/main/Cargo.toml b/examples/qml_multi_crates/rust/main/Cargo.toml similarity index 89% rename from examples/meta_project/rust/main/Cargo.toml rename to examples/qml_multi_crates/rust/main/Cargo.toml index bfef3cd8f..9032870a0 100644 --- a/examples/meta_project/rust/main/Cargo.toml +++ b/examples/qml_multi_crates/rust/main/Cargo.toml @@ -3,14 +3,14 @@ # # SPDX-License-Identifier: MIT OR Apache-2.0 [package] -name = "qml_meta_project" +name = "qml_multi_crates" version = "0.1.0" authors = ["Andrew Hayzen "] edition = "2021" license = "MIT OR Apache-2.0" -# [lib] -# crate-type = ["staticlib", "lib"] +[lib] +crate-type = ["staticlib", "lib"] [dependencies] sub1 = { path = "../sub1" } diff --git a/examples/meta_project/rust/main/build.rs b/examples/qml_multi_crates/rust/main/build.rs similarity index 100% rename from examples/meta_project/rust/main/build.rs rename to examples/qml_multi_crates/rust/main/build.rs diff --git a/examples/meta_project/rust/main/src/lib.rs b/examples/qml_multi_crates/rust/main/src/lib.rs similarity index 100% rename from examples/meta_project/rust/main/src/lib.rs rename to examples/qml_multi_crates/rust/main/src/lib.rs diff --git a/examples/meta_project/rust/main/src/main.rs b/examples/qml_multi_crates/rust/main/src/main.rs similarity index 91% rename from examples/meta_project/rust/main/src/main.rs rename to examples/qml_multi_crates/rust/main/src/main.rs index 2281f609b..eef98fc65 100644 --- a/examples/meta_project/rust/main/src/main.rs +++ b/examples/qml_multi_crates/rust/main/src/main.rs @@ -3,12 +3,12 @@ // // SPDX-License-Identifier: MIT OR Apache-2.0 -extern crate qml_meta_project; +extern crate qml_multi_crates; use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QUrl}; fn main() { - cxx_qt::init_crate!(qml_meta_project); + cxx_qt::init_crate!(qml_multi_crates); cxx_qt::init_qml_module!("com.kdab.cxx_qt.demo"); // Create the application and engine @@ -42,7 +42,7 @@ mod tests { // Otherwise linking will fail! #[test] fn init_dependencies() { - cxx_qt::init_crate!(qml_meta_project); + cxx_qt::init_crate!(qml_multi_crates); cxx_qt::init_qml_module!("com.kdab.cxx_qt.demo"); } } diff --git a/examples/meta_project/rust/main/src/main_object.rs b/examples/qml_multi_crates/rust/main/src/main_object.rs similarity index 100% rename from examples/meta_project/rust/main/src/main_object.rs rename to examples/qml_multi_crates/rust/main/src/main_object.rs diff --git a/examples/meta_project/rust/sub1/Cargo.toml b/examples/qml_multi_crates/rust/sub1/Cargo.toml similarity index 100% rename from examples/meta_project/rust/sub1/Cargo.toml rename to examples/qml_multi_crates/rust/sub1/Cargo.toml diff --git a/examples/meta_project/rust/sub1/build.rs b/examples/qml_multi_crates/rust/sub1/build.rs similarity index 100% rename from examples/meta_project/rust/sub1/build.rs rename to examples/qml_multi_crates/rust/sub1/build.rs diff --git a/examples/meta_project/rust/sub1/src/lib.rs b/examples/qml_multi_crates/rust/sub1/src/lib.rs similarity index 100% rename from examples/meta_project/rust/sub1/src/lib.rs rename to examples/qml_multi_crates/rust/sub1/src/lib.rs diff --git a/examples/meta_project/rust/sub1/src/sub1_object.rs b/examples/qml_multi_crates/rust/sub1/src/sub1_object.rs similarity index 100% rename from examples/meta_project/rust/sub1/src/sub1_object.rs rename to examples/qml_multi_crates/rust/sub1/src/sub1_object.rs diff --git a/examples/meta_project/rust/sub2/Cargo.toml b/examples/qml_multi_crates/rust/sub2/Cargo.toml similarity index 100% rename from examples/meta_project/rust/sub2/Cargo.toml rename to examples/qml_multi_crates/rust/sub2/Cargo.toml diff --git a/examples/meta_project/rust/sub2/build.rs b/examples/qml_multi_crates/rust/sub2/build.rs similarity index 100% rename from examples/meta_project/rust/sub2/build.rs rename to examples/qml_multi_crates/rust/sub2/build.rs diff --git a/examples/meta_project/rust/sub2/src/lib.rs b/examples/qml_multi_crates/rust/sub2/src/lib.rs similarity index 100% rename from examples/meta_project/rust/sub2/src/lib.rs rename to examples/qml_multi_crates/rust/sub2/src/lib.rs diff --git a/examples/meta_project/rust/sub2/src/sub2_object.rs b/examples/qml_multi_crates/rust/sub2/src/sub2_object.rs similarity index 100% rename from examples/meta_project/rust/sub2/src/sub2_object.rs rename to examples/qml_multi_crates/rust/sub2/src/sub2_object.rs From 5a82863e116d2ac0a4502b673a28f61c1295db77 Mon Sep 17 00:00:00 2001 From: Leon Matthes Date: Wed, 12 Feb 2025 10:58:18 +0100 Subject: [PATCH 8/9] Update Changelog and documentation --- CHANGELOG.md | 2 + book/src/internals/build-system.md | 64 +++++++++++++++--------------- 2 files changed, 35 insertions(+), 31 deletions(-) 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/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. From 227cad2dd70d32c97994bab562eb02827a3b1dd8 Mon Sep 17 00:00:00 2001 From: Leon Matthes Date: Fri, 14 Feb 2025 16:32:13 +0100 Subject: [PATCH 9/9] Initialize crates QML modules in crate initializer --- Cargo.lock | 33 +++++++++++++++++++ crates/cxx-qt-build/src/lib.rs | 16 ++------- crates/qt-build-utils/src/lib.rs | 9 +++++ .../qml_multi_crates/rust/main/src/main.rs | 1 - 4 files changed, 45 insertions(+), 14 deletions(-) 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/crates/cxx-qt-build/src/lib.rs b/crates/cxx-qt-build/src/lib.rs index 8f7d1a600..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, @@ -897,7 +896,7 @@ impl CxxQtBuilder { &module_init_key, ); - initializer_functions.push(public_initializer); + initializer_functions.push(public_initializer.strip_file()); } initializer_functions } @@ -1153,6 +1152,7 @@ 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::>(); @@ -1179,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/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/qml_multi_crates/rust/main/src/main.rs b/examples/qml_multi_crates/rust/main/src/main.rs index eef98fc65..8354191c3 100644 --- a/examples/qml_multi_crates/rust/main/src/main.rs +++ b/examples/qml_multi_crates/rust/main/src/main.rs @@ -9,7 +9,6 @@ use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QUrl}; fn main() { cxx_qt::init_crate!(qml_multi_crates); - cxx_qt::init_qml_module!("com.kdab.cxx_qt.demo"); // Create the application and engine let mut app = QGuiApplication::new();