Skip to content

Commit

Permalink
Use method calls to initialize dependencies
Browse files Browse the repository at this point in the history
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 KDAB#1166
  • Loading branch information
LeonMatthesKDAB committed Jan 29, 2025
1 parent 5229e31 commit b481fca
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 151 deletions.
29 changes: 15 additions & 14 deletions crates/cxx-qt-build/src/dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use std::path::{Path, PathBuf};
/// These are all specified by this Interface struct, which should be passed to the [crate::CxxQtBuilder::library] function.
pub struct Interface {
pub(crate) compile_definitions: HashMap<String, Option<String>>,
pub(crate) initializers: Vec<PathBuf>,
pub(crate) initializers: Vec<String>,
// The name of the links keys, whose CXX-Qt dependencies to reexport
pub(crate) reexport_links: HashSet<String>,
pub(crate) exported_include_prefixes: Vec<String>,
Expand Down Expand Up @@ -65,18 +65,19 @@ impl Interface {
self
}

/// Add a C++ file path that will be exported as an initializer to downstream dependencies.
/// Register an initializer function to be called on startup.
///
/// Initializer files will be built into object files, instead of linked into the static
/// library.
/// This way, the static variables and their constructors in this code will not be optimized
/// out by the linker.
pub fn initializer(mut self, path: impl AsRef<Path>) -> Self {
let path = PathBuf::from(path.as_ref());
let path = path
.canonicalize()
.expect("Failed to canonicalize path to initializer! Does the path exist?");
self.initializers.push(path);
/// The initializer function must match the C++ signature:
/// extern "C" int function_name();
/// or the Rust equivalent:
/// extern "C" fn function_name() -> i32;
///
/// Where function_name is the parameter passed here.
///
/// Note that the function_name must be unique across all dependencies and must be accessible
/// directly by the linker without additional name mangling.
pub fn initializer(mut self, function_name: String) -> Self {
self.initializers.push(function_name);
self
}

Expand Down Expand Up @@ -153,7 +154,7 @@ pub(crate) struct Manifest {
pub(crate) link_name: String,
pub(crate) qt_modules: Vec<String>,
pub(crate) defines: Vec<(String, Option<String>)>,
pub(crate) initializers: Vec<PathBuf>,
pub(crate) initializers: Vec<String>,
pub(crate) exported_include_prefixes: Vec<String>,
}

Expand Down Expand Up @@ -205,7 +206,7 @@ impl Dependency {
pub(crate) fn initializer_paths(
interface: Option<&Interface>,
dependencies: &[Dependency],
) -> HashSet<PathBuf> {
) -> HashSet<String> {
dependencies
.iter()
.flat_map(|dep| dep.manifest.initializers.iter().cloned())
Expand Down
121 changes: 81 additions & 40 deletions crates/cxx-qt-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,6 @@ pub struct CxxQtBuilder {
cc_builder: cc::Build,
public_interface: Option<Interface>,
include_prefix: String,
initializers: Vec<String>,
}

impl CxxQtBuilder {
Expand Down Expand Up @@ -396,7 +395,6 @@ impl CxxQtBuilder {
qt_modules,
qml_modules: vec![],
cc_builder: cc::Build::new(),
initializers: vec![],
public_interface: None,
include_prefix: crate_name(),
}
Expand Down Expand Up @@ -888,55 +886,91 @@ impl CxxQtBuilder {
}
}

fn setup_qt5_compatibility(&mut self, qtbuild: &qt_build_utils::QtBuild) {
// If we are using Qt 5 then write the std_types source
// This registers std numbers as a type for use in QML
//
// Note that we need this to be compiled into an object file
// as they are stored in statics in the source.
//
// TODO: Can we move this into cxx-qt so that it's only built
// once rather than for every cxx-qt-build? When we do this
// ensure that in a multi project that numbers work everywhere.
//
// Also then it should be possible to use CARGO_MANIFEST_DIR/src/std_types_qt5.cpp
// as path for cc::Build rather than copying the .cpp file
//
// https://github.com/rust-lang/rust/issues/108081
// https://github.com/KDAB/cxx-qt/pull/598
fn init_function_name(&self) -> String {
format!("cxx_qt_init_{}", crate_name().replace("-", "_"))
}

fn generate_init_code(
&self,
qtbuild: &qt_build_utils::QtBuild,
initializer_files: &[PathBuf],
dependency_initializers: &HashSet<String>,
) -> String {
let mut qt5_initializer = None;
if qtbuild.version().major == 5 {
self.initializers
.push(include_str!("std_types_qt5.cpp").to_owned());
// If we are using Qt 5 then write the std_types source
// This registers std numbers as a type for use in QML
//
// Note that we need this to be compiled into an object file
// as they are stored in statics in the source.
//
// See also:
// https://github.com/rust-lang/rust/issues/108081
// https://github.com/KDAB/cxx-qt/pull/598
qt5_initializer = Some(include_str!("std_types_qt5.cpp").to_owned());
}
}

fn generate_init_code(&self, initializers: &HashSet<PathBuf>) -> String {
initializers
let (declarations, calls): (Vec<_>, Vec<_>) = dependency_initializers
.iter()
.map(|init_fun| {
(
// declaration
format!("extern \"C\" int {init_fun}();"),
// call
format!("{init_fun}();"),
)
})
.unzip();
let init_function = format!(
"{declarations}\nextern \"C\" int {init_fun}() {{\n{calls}\nreturn 42;\n}}",
init_fun = self.init_function_name(),
declarations = declarations.join("\n"),
calls = calls.join("\n"),
);

initializer_files
.iter()
.map(|path| std::fs::read_to_string(path).expect("Could not read initializer file!"))
.chain(self.initializers.iter().cloned())
.map(|file| std::fs::read_to_string(file).expect("Could not read initializer file"))
.chain(qt5_initializer)
.chain(Some(init_function))
.collect::<Vec<_>>()
.join("\n")
}

fn build_initializers(&mut self, init_builder: &cc::Build, initializers: &HashSet<PathBuf>) {
fn build_initializers(&mut self, init_builder: &cc::Build, initializer_code: String) {
let initializers_path = dir::out().join("cxx-qt-build").join("initializers");
std::fs::create_dir_all(&initializers_path).expect("Failed to create initializers path!");

let initializers_path = initializers_path.join(format!("{}.cpp", crate_name()));
std::fs::write(&initializers_path, self.generate_init_code(initializers))
let initializers_file = initializers_path.join(format!("{}-init-lib.cpp", crate_name()));
std::fs::write(&initializers_file, initializer_code)
.expect("Could not write initializers file");

// Build static initializers into a library that we link with whole-archive
// Then use object files just to trigger the initial call.
init_builder
.clone()
.file(initializers_file)
.compile(&format!("{}-initializers", crate_name()));

let init_call = format!(
"extern \"C\" int {init_fun}();\nstatic const int do_{init_fun} = {init_fun}();",
init_fun = self.init_function_name()
);

let init_file = initializers_path.join(format!("{}.cpp", crate_name()));
std::fs::write(&init_file, init_call).expect("Could not write initializers call file!");

Self::build_object_file(
init_builder,
initializers_path,
init_file,
dir::crate_target().join("initializers.o"),
);
}

fn generate_cpp_from_qrc_files(
&mut self,
qtbuild: &mut qt_build_utils::QtBuild,
) -> HashSet<PathBuf> {
) -> Vec<(PathBuf, String)> {
self.qrc_files
.iter()
.map(|qrc_file| {
Expand All @@ -955,7 +989,7 @@ impl CxxQtBuilder {
&self,
dependencies: &[Dependency],
qt_modules: HashSet<String>,
initializers: HashSet<PathBuf>,
initializers: HashSet<String>,
) {
if let Some(interface) = &self.public_interface {
// We automatically reexport all qt_modules and initializers from downstream dependencies
Expand Down Expand Up @@ -1084,15 +1118,22 @@ impl CxxQtBuilder {
&self.include_prefix.clone(),
);

let mut initializers = self.generate_cpp_from_qrc_files(&mut qtbuild);
initializers.extend(dependencies::initializer_paths(
self.public_interface.as_ref(),
&dependencies,
));

self.setup_qt5_compatibility(&qtbuild);

self.build_initializers(&init_builder, &initializers);
let (qrc_files, qrc_initializers): (Vec<PathBuf>, HashSet<String>) = self
.generate_cpp_from_qrc_files(&mut qtbuild)
.into_iter()
.unzip();

let dependency_initializers =
dependencies::initializer_paths(self.public_interface.as_ref(), &dependencies);
let initializers = dependency_initializers
.union(&qrc_initializers)
.cloned()
.chain(Some(self.init_function_name()))
.collect();

let initializer_code =
self.generate_init_code(&qtbuild, &qrc_files, &dependency_initializers);
self.build_initializers(&init_builder, initializer_code);

// Only compile if we have added files to the builder
// otherwise we end up with no static library but ask cargo to link to it which causes an error
Expand Down
6 changes: 4 additions & 2 deletions crates/cxx-qt-lib/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ fn main() {
}

let mut cpp_files = vec![
"core/init",
"core/qbytearray",
"core/qcoreapplication",
"core/qdate",
Expand Down Expand Up @@ -288,6 +289,7 @@ fn main() {

if qt_gui_enabled() {
cpp_files.extend([
"gui/init",
"gui/qcolor",
"gui/qfont",
"gui/qguiapplication",
Expand Down Expand Up @@ -317,15 +319,15 @@ fn main() {
}

let mut interface = cxx_qt_build::Interface::default()
.initializer("src/core/init.cpp")
.initializer("init_cxx_qt_lib_core".to_owned())
.export_include_prefixes([])
.export_include_directory(header_dir(), "cxx-qt-lib")
.reexport_dependency("cxx-qt");

if qt_gui_enabled() {
interface = interface
.define("CXX_QT_GUI_FEATURE", None)
.initializer("src/gui/init.cpp");
.initializer("init_cxx_qt_lib_gui".to_owned());
}

if qt_qml_enabled() {
Expand Down
Loading

0 comments on commit b481fca

Please sign in to comment.