The CXX-Qt Build System
Building with CXX-Qt is somewhat more complicated than it may sound at first.
The problems (or "challenges" if you prefer corporate jargon 😉)
We unfortunately cannot simply link your Rust code into a static library and link to it for the following reasons:
Static Initializers
Qt code often contains initialization code that is called by a static variable that runs the initialization code in its constructor.
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.
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
Somehow, all of this should be compatible with both CMake, and Cargo-only builds.
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.
We want to use a similar approach to CXX, which uses Cargos links key to ensure a correct build order (see the links key documentation).
When building with cxx-qt-build, you may simply specify that your code depends on another crate.
Cargo will then make sure that the build scripts of the dependencies have run before the build script of this crate.
We can additionally pass metadata between build scripts, which we use to find the manifest.json of each crate and the path to their "target" directory.
The "target" directory
Each build script can export artifacts into a folder with a well-known layout.
It is also required to export a manifest.json file that tells downstream dependencies which of these artifacts to include and how to configure their own build.
This "target" directory is usually in the OUT_DIR, but can be exported using CXX_QT_EXPORT_DIR and CXX_QT_EXPORT_CRATE_[crate-name] environment variables.
Which is used by CMake to import the artifacts. (See: Integration with CMake)
crates directory
Inside the target directory, there should be a crates folder with one subfolder per crate.
Each crates subfolder should contain the following:
include/crate-name- A folder for all headers that are exported by this cratecxx-qt-lib -> <path-to-dependency>/include/cxx-qt-lib- Symbolic links for every dependency
manifest.json- This file describes which headers this library makes available, if it needs any Qt modules, etc.initializers.o- The initializers of this crate + all it's dependencies to be linked in by CMake
Via the manifest.json, we are then able to figure out which header paths of this dependency to include, which Qt modules to link, etc.
To make sure the correct data ends up in the manifest.json, we provide the cxx_qt_build::Interface struct which uses the builder pattern to specify all the necessary data.
qml_modules directory
Next to the crates directory, there should be a qml_modules directory, which contains one directory per declared QML module.
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.
CMake can then expect required artifacts to exist at pre-defined locations, which can be added as dependency, include directories, objects, etc. to the Crate target.
We will rely on Corrosion to import the crate and provide targets for it.
However, we also want to provide some custom functions that wrap corrosion and set up the import of our own artifacts.
Currently we provide two functions:
- cxx_qt_import_crate
- A wrapper over corrosion_import_crate that defines the
CXXQT_EXPORT_DIR, imports the initializers object files, etc.
- A wrapper over corrosion_import_crate that defines the
- cxx_qt_import_qml_module
- Import a given QML module by URI from the given SOURCE_CRATE and provide it as a target.