Skip to content

Commit de942a1

Browse files
Use method calls to initialize dependencies
This speeds up rebuilds, as we only need to link to upstream dependencies initializers, not build their initializers every time. It should also allow us to manually initialize certain libraries down the line with something similar to Q_INIT_RESOURCE, etc. Closes #1166
1 parent 90039d7 commit de942a1

File tree

12 files changed

+364
-261
lines changed

12 files changed

+364
-261
lines changed

Diff for: book/src/internals/build-system.md

+24-3
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,34 @@ Qt code often contains initialization code that is called by a static variable t
1919

2020
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.
2121

22-
There are two ways to solve this:
22+
There are multiple ways to solve this:
2323

2424
- Export an object file and link that to the main binary. Object files are always included completely
2525
- Use the whole-archive linker flag which forces inclusion of every object within the static library.
2626
- 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.
2727
- This has caused some recent regressions with Rust 1.78+, where MSVC could no longer link CXX-Qt due to duplicate symbols
2828
- The way to solve this is to only export the static initializers as a library and link that into CMake.
29+
- Manually calling the static initializer code
30+
- This is basically what Q_INIT_RESOURCE and Q_IMPORT_PLUGIN do
31+
- 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.
32+
33+
At the moment we employ a mix of all methods.
34+
35+
First and foremost, we wrap all our initializers into functions with well-defined names (starting with `cxx_qt_init`) and C-compatible signatures.
36+
This allows us to manually call the initializers from any point in the linker chain, which forces their inclusion.
37+
These initializer functions call the initializer functions from their upstream dependencies so that the entire dependency tree is initialized.
38+
39+
However, we don't want to have to call the initializers manually in every resulting binary.
40+
To solve this, we use static initializers that simply call the initializer function of the crate/Qml module, thereby initializing all dependencies.
41+
As noted earlier, these static initializers are routinely optimized out by the linker.
42+
43+
For Cargo builds we prevent this by linking all initializers with +whole-archive which forces all of them to be included.
44+
Experience has shown that this gives us the best compatibility overall, as linking object files to Cargo builds turned out to be quite finicky.
45+
As the initializers contain very few symbols themselves, this should also rarely lead to issues with duplicate symbols.
46+
47+
In CMake we mirror Qts behavior, which is to build the static initializer as an `OBJECT` library.
48+
The initializer functions themselves are still built into the Rust static library and the `OBJECT` library must therefore link to it.
49+
This is taken care of by the `cxx_qt_import_crate`/`_import_qml_module` functions.
2950

3051
### Header files
3152

@@ -87,7 +108,7 @@ However, we also want to provide some custom functions that wrap corrosion and s
87108

88109
Currently we provide two functions:
89110

90-
- cxxqt_import_crate
111+
- cxx_qt_import_crate
91112
- A wrapper over corrosion_import_crate that defines the `CXXQT_EXPORT_DIR`, imports the initializers object files, etc.
92-
- cxxqt_import_qml_module
113+
- cxx_qt_import_qml_module
93114
- Import a given QML module by URI from the given SOURCE_CRATE and provide it as a target.

Diff for: crates/cxx-qt-build/Cargo.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ cxx-gen.workspace = true
1919
cxx-qt-gen.workspace = true
2020
proc-macro2.workspace = true
2121
quote.workspace = true
22-
qt-build-utils.workspace = true
22+
qt-build-utils = { workspace = true, features = ["serde"] }
2323
codespan-reporting = "0.11"
2424
version_check = "0.9"
25-
serde = { version = "1.0", features = ["default", "derive"] }
25+
serde.workspace = true
2626
serde_json = "1.0"
2727

2828
[features]

Diff for: crates/cxx-qt-build/src/dependencies.rs

+2-27
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ use std::path::{Path, PathBuf};
1313
/// When generating a library with cxx-qt-build, the library may need to export certain flags or headers.
1414
/// These are all specified by this Interface struct, which should be passed to the [crate::CxxQtBuilder::library] function.
1515
pub struct Interface {
16-
pub(crate) initializers: Vec<PathBuf>,
1716
// The name of the links keys, whose CXX-Qt dependencies to reexport
1817
pub(crate) reexport_links: HashSet<String>,
1918
pub(crate) exported_include_prefixes: Vec<String>,
@@ -28,7 +27,6 @@ pub struct Interface {
2827
impl Default for Interface {
2928
fn default() -> Self {
3029
Self {
31-
initializers: Vec::new(),
3230
reexport_links: HashSet::new(),
3331
exported_include_prefixes: vec![super::crate_name()],
3432
exported_include_directories: Vec::new(),
@@ -37,21 +35,6 @@ impl Default for Interface {
3735
}
3836

3937
impl Interface {
40-
/// Add a C++ file path that will be exported as an initializer to downstream dependencies.
41-
///
42-
/// Initializer files will be built into object files, instead of linked into the static
43-
/// library.
44-
/// This way, the static variables and their constructors in this code will not be optimized
45-
/// out by the linker.
46-
pub fn initializer(mut self, path: impl AsRef<Path>) -> Self {
47-
let path = PathBuf::from(path.as_ref());
48-
let path = path
49-
.canonicalize()
50-
.expect("Failed to canonicalize path to initializer! Does the path exist?");
51-
self.initializers.push(path);
52-
self
53-
}
54-
5538
/// Export all headers with the given prefix to downstream dependencies
5639
///
5740
/// Note: This will overwrite any previously specified header_prefixes, including the default
@@ -124,7 +107,7 @@ pub(crate) struct Manifest {
124107
pub(crate) name: String,
125108
pub(crate) link_name: String,
126109
pub(crate) qt_modules: Vec<String>,
127-
pub(crate) initializers: Vec<PathBuf>,
110+
pub(crate) initializers: Vec<qt_build_utils::Initializer>,
128111
pub(crate) exported_include_prefixes: Vec<String>,
129112
}
130113

@@ -173,18 +156,10 @@ impl Dependency {
173156
}
174157
}
175158

176-
pub(crate) fn initializer_paths(
177-
interface: Option<&Interface>,
178-
dependencies: &[Dependency],
179-
) -> HashSet<PathBuf> {
159+
pub(crate) fn initializers(dependencies: &[Dependency]) -> Vec<qt_build_utils::Initializer> {
180160
dependencies
181161
.iter()
182162
.flat_map(|dep| dep.manifest.initializers.iter().cloned())
183-
.chain(
184-
interface
185-
.iter()
186-
.flat_map(|interface| interface.initializers.iter().cloned()),
187-
)
188163
.collect()
189164
}
190165

Diff for: crates/cxx-qt-build/src/dir.rs

+6
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ pub(crate) fn is_exporting() -> bool {
9191
export().is_some()
9292
}
9393

94+
pub(crate) fn initializers(key: &str) -> PathBuf {
95+
let path = out().join("cxx-qt-build").join("initializers").join(key);
96+
std::fs::create_dir_all(&path).expect("Failed to create initializers path!");
97+
path
98+
}
99+
94100
#[cfg(unix)]
95101
pub(crate) fn symlink_or_copy_directory(
96102
source: impl AsRef<Path>,

0 commit comments

Comments
 (0)