Skip to content

Commit

Permalink
register QML elements at build time
Browse files Browse the repository at this point in the history
by specifying
[cxxqt::qobject(qml_uri = "foo.bar", qml_verison = "1.0")]

Fixes KDAB#241
  • Loading branch information
vimpostor authored and Be-ing committed Feb 8, 2023
1 parent 202889f commit 10e8ba8
Show file tree
Hide file tree
Showing 33 changed files with 536 additions and 234 deletions.
3 changes: 3 additions & 0 deletions book/src/getting-started/3-exposing-to-qml.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ That means you can also use it like any other C++ class.
Derive from it, connect signals and slots to it, put it in a QVector, do whatever you want with it.
That's the power of CXX-Qt.

For [CMake builds](./4-qml-gui.md), note that the QML element cannot be registered at build time with the [qt_add_qml_module](https://doc.qt.io/qt-6/qt-add-qml-module.html) CMake function.
This is because the generated header is generated at CMake build time, but `qt_add_qml_module` needs it at CMake configure time.
For [Cargo builds](./6-cargo-executable.md), the Rust build script automatically registers the QML element just like the `qt_add_qml_module` CMake function.

As we later want to include our QML GUI in a `main.qml` file inside the [Qt resource system](https://doc.qt.io/qt-6/resources.html), we'll have to add a `qml.qrc` file in the `qml` folder as well:
```qrc,ignore
Expand Down
35 changes: 4 additions & 31 deletions book/src/getting-started/6-cargo-executable.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ SPDX-License-Identifier: MIT OR Apache-2.0

In this example, we will demonstrate how to build the same `cxxqt_object.rs` module and QML file as the
previous example, but without using CMake or another C++ build system. Cargo will do the entire build
just like a typical Rust application. Because cxx-qt does not bind the entire Qt API, we will still
need to write a bit of C++ code. However, we'll use the cxx-qt-build crate to compile it instead of CMake.
just like a typical Rust application.

Note that the folder structure of this example is different to the CMake tutorial, this is because the `Cargo.toml` is now in the root. So there isn't a `rust` folder, instead just a `src` folder and the `.rs` files have moved up one folder.

Expand All @@ -36,23 +35,6 @@ The `build.rs` script is similar. However, without CMake, CxxQtBuilder needs to
Refer to the [CxxQtBuilder](https://docs.rs/cxx-qt-build/latest/cxx_qt_build/struct.CxxQtBuilder.html)
and [cc::Build](https://docs.rs/cc/latest/cc/struct.Build.html) documentation for further details.

## C++ shim

We need to write a small C++ shim to register QML types. Later we will call this from the Rust executable.

First, we need to include Qt headers and the C++ header generated from `src/cxxqt_object.rs`.

```c++,ignore
{{#include ../../../examples/cargo_without_cmake/cpp/register_types.cpp:book_cargo_cpp_includes}}
```

Now create a `registerTypes` method which uses the included QObject to register
with the `QQmlEngine`.

```c++,ignore
{{#include ../../../examples/cargo_without_cmake/cpp/register_types.cpp:book_cargo_register_types}}
```

## Rust executable

Instead of a `src/lib.rs` file, this time we need a `src/main.rs` file for Cargo to build the Rust code
Expand All @@ -63,22 +45,13 @@ will need to call C++:
{{#include ../../../examples/cargo_without_cmake/src/main.rs:book_cargo_imports}}
```

Now create a file called `src/qml.rs` this will contain a bridge which allows
us to initialize the Qt resources and register the QML types.

```rust,ignore
{{#include ../../../examples/cargo_without_cmake/src/qml.rs:book_cargo_qml_bridge}}
```

Define the `main` function that will be called when the executable starts.
This performs the following tasks
Define the `main` function that will be called when the executable starts. This works just like starting a QML
application in C++:

* Initialize the Qt resources
* Create a `QGuiApplication`
* Create a `QQmlApplicationEngine`
* Register the QML types to the engine
* Set the QML file path to the engine
* Start the application
* Run the application

```rust,ignore
{{#include ../../../examples/cargo_without_cmake/src/main.rs:book_cargo_rust_main}}
Expand Down
135 changes: 95 additions & 40 deletions crates/cxx-qt-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use convert_case::{Case, Casing};
use quote::ToTokens;
use std::{
collections::HashSet,
collections::{HashMap, HashSet},
env,
fs::File,
io::Write,
Expand All @@ -16,7 +16,7 @@ use std::{

use cxx_qt_gen::{
parse_qt_file, write_cpp, write_rust, CppFragment, CxxQtItem, GeneratedCppBlocks,
GeneratedRustBlocks, Parser,
GeneratedRustBlocks, Parser, QmlElementMetadata,
};

// TODO: we need to eventually support having multiple modules defined in a single file. This
Expand All @@ -30,13 +30,19 @@ use cxx_qt_gen::{
struct GeneratedCppFilePaths {
plain_cpp: PathBuf,
qobject: Option<PathBuf>,
qobject_header: Option<PathBuf>,
qobject_header: Option<QObjectHeader>,
}

struct QObjectHeader {
path: PathBuf,
qml_metadata: Vec<QmlElementMetadata>,
}

struct GeneratedCpp {
cxx_qt: Option<CppFragment>,
cxx: cxx_gen::GeneratedCode,
file_ident: String,
qml_metadata: Vec<QmlElementMetadata>,
}

impl GeneratedCpp {
Expand All @@ -46,6 +52,7 @@ impl GeneratedCpp {
let file = parse_qt_file(rust_file_path).unwrap();

let mut cxx_qt = None;
let mut qml_metadata = Vec::new();
// TODO: later change how the resultant filename is chosen, can we match the input file like
// CXX does?
//
Expand Down Expand Up @@ -92,6 +99,11 @@ impl GeneratedCpp {
let generated_rust = GeneratedRustBlocks::from(&parser).unwrap();
let rust_tokens = write_rust(&generated_rust);
file_ident = parser.cxx_file_stem.clone();
for (_, qobject) in parser.cxx_qt_data.qobjects {
if let Some(q) = qobject.qml_metadata {
qml_metadata.push(q);
}
}

// We need to do this and can't rely on the macro, as we need to generate the
// CXX bridge Rust code that is then fed into the cxx_gen generation.
Expand All @@ -111,12 +123,13 @@ impl GeneratedCpp {
cxx_qt,
cxx,
file_ident,
qml_metadata,
}
}

/// Write generated .cpp and .h files to specified directories. Returns the paths of all files written.
pub fn write_to_directories(
&self,
self,
cpp_directory: impl AsRef<Path>,
header_directory: impl AsRef<Path>,
) -> GeneratedCppFilePaths {
Expand Down Expand Up @@ -148,7 +161,10 @@ impl GeneratedCpp {
header
.write_all(header_generated.as_bytes())
.expect("Could not write cxx-qt header file");
cpp_file_paths.qobject_header = Some(header_path);
cpp_file_paths.qobject_header = Some(QObjectHeader {
path: header_path,
qml_metadata: self.qml_metadata,
});

let cpp_path = PathBuf::from(format!(
"{}/{}.cxxqt.cpp",
Expand Down Expand Up @@ -245,7 +261,7 @@ fn generate_cxxqt_cpp_files(
#[derive(Default)]
pub struct CxxQtBuilder {
rust_sources: Vec<PathBuf>,
qobject_headers: Vec<PathBuf>,
qobject_headers: Vec<QObjectHeader>,
qrc_files: Vec<PathBuf>,
qt_modules: HashSet<String>,
cc_builder: cc::Build,
Expand Down Expand Up @@ -317,7 +333,10 @@ impl CxxQtBuilder {
/// This allows building QObject C++ subclasses besides the ones autogenerated by cxx-qt.
pub fn qobject_header(mut self, path: impl AsRef<Path>) -> Self {
let path = path.as_ref();
self.qobject_headers.push(path.to_path_buf());
self.qobject_headers.push(QObjectHeader {
path: path.to_owned(),
qml_metadata: Vec::new(),
});
println!("cargo:rerun-if-changed={}", path.display());
self
}
Expand Down Expand Up @@ -351,40 +370,21 @@ impl CxxQtBuilder {
/// Generate and compile cxx-qt C++ code, as well as compile any additional files from
/// [CxxQtBuilder::qobject_header] and [CxxQtBuilder::cc_builder].
pub fn build(mut self) {
self.cc_builder.cpp(true);
// MSVC
self.cc_builder.flag_if_supported("/std:c++17");
self.cc_builder.flag_if_supported("/Zc:__cplusplus");
self.cc_builder.flag_if_supported("/permissive-");
// GCC + Clang
self.cc_builder.flag_if_supported("-std=c++17");

// Enable Qt Gui in C++ if the feature is enabled
#[cfg(feature = "qt_gui")]
self.cc_builder.define("CXX_QT_GUI_FEATURE", None);
// Enable Qt Gui in C++ if the feature is enabled
#[cfg(feature = "qt_qml")]
self.cc_builder.define("CXX_QT_QML_FEATURE", None);

let mut qtbuild = qt_build_utils::QtBuild::new(self.qt_modules.into_iter().collect())
.expect("Could not find Qt installation");
qtbuild.cargo_link_libraries();
for include_dir in qtbuild.include_paths() {
self.cc_builder.include(include_dir);
}

let out_dir = env::var("OUT_DIR").unwrap();
// The include directory needs to be namespaced by crate name when exporting for a C++ build system,
// but for using cargo build without a C++ build system, OUT_DIR is already namespaced by crate name.
let header_root = match env::var("CXXQT_EXPORT_DIR") {
Ok(export_dir) => format!("{export_dir}/{}", env::var("CARGO_PKG_NAME").unwrap()),
Err(_) => env::var("OUT_DIR").unwrap(),
Err(_) => out_dir,
};
self.cc_builder.include(&header_root);
let generated_header_dir = format!("{header_root}/cxx-qt-gen");

cxx_qt_lib_headers::write_headers(format!("{header_root}/cxx-qt-lib"));
let mut qtbuild = qt_build_utils::QtBuild::new(self.qt_modules.into_iter().collect())
.expect("Could not find Qt installation");
qtbuild.cargo_link_libraries();

// Write cxx header
// Write cxx-qt-lib and cxx headers
cxx_qt_lib_headers::write_headers(format!("{header_root}/cxx-qt-lib"));
std::fs::create_dir_all(format!("{header_root}/rust"))
.expect("Could not create cxx header directory");
let h_path = format!("{header_root}/rust/cxx.h");
Expand All @@ -396,25 +396,80 @@ impl CxxQtBuilder {
}

// Generate files
for files in generate_cxxqt_cpp_files(&self.rust_sources, generated_header_dir) {
for files in generate_cxxqt_cpp_files(&self.rust_sources, &generated_header_dir) {
self.cc_builder.file(files.plain_cpp);
if let (Some(qobject), Some(qobject_header)) = (files.qobject, files.qobject_header) {
self.cc_builder.file(&qobject);
self.qobject_headers.push(qobject_header);
}
}

// Setup compiler
// Static QML plugin and Qt resource initialization need to be linked with +whole-archive
// 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 linked with +whole-archive
// to avoid bloating the binary.
let mut cc_builder_whole_archive = cc::Build::new();
cc_builder_whole_archive.link_lib_modifier("+whole-archive");
for builder in [&mut self.cc_builder, &mut cc_builder_whole_archive] {
// Setup compiler
builder.cpp(true);
// MSVC
builder.flag_if_supported("/std:c++17");
builder.flag_if_supported("/Zc:__cplusplus");
builder.flag_if_supported("/permissive-");
// GCC + Clang
builder.flag_if_supported("-std=c++17");
// Enable Qt Gui in C++ if the feature is enabled
#[cfg(feature = "qt_gui")]
builder.define("CXX_QT_GUI_FEATURE", None);
// Enable Qt Gui in C++ if the feature is enabled
#[cfg(feature = "qt_qml")]
builder.define("CXX_QT_QML_FEATURE", None);
for include_dir in qtbuild.include_paths() {
builder.include(&include_dir);
}
builder.include(&header_root);
builder.include(&generated_header_dir);
}

// Compile files
let mut qml_modules = HashMap::<(String, usize, usize), Vec<PathBuf>>::new();
let mut cc_builder_whole_archive_files_added = false;
// Run moc on C++ headers with Q_OBJECT macro
for qobject_header in self.qobject_headers {
self.cc_builder.file(qtbuild.moc(&qobject_header));
let moc_products = qtbuild.moc(&qobject_header.path);
self.cc_builder.file(moc_products.cpp);
for qml_metadata in qobject_header.qml_metadata {
self.cc_builder.define("QT_STATICPLUGIN", None);
qml_modules
.entry((
qml_metadata.uri.clone(),
qml_metadata.version_major,
qml_metadata.version_minor,
))
.or_default()
.push(moc_products.metatypes_json.clone());
}
}
for ((uri, version_major, version_minor), paths) in qml_modules {
let qml_type_registration_files =
qtbuild.register_qml_types(&paths, version_major, version_minor, &uri);
self.cc_builder
.file(qml_type_registration_files.qmltyperegistrar);
self.cc_builder.file(qml_type_registration_files.plugin);
cc_builder_whole_archive.file(qml_type_registration_files.plugin_init);
cc_builder_whole_archive_files_added = true;
}

// Generate code from .qrc files, but do not compile it. Instead, the user needs to #include them
// in a .cpp file. Otherwise, MSVC won't link if the generated C++ is built separately.
for qrc_file in self.qrc_files {
qtbuild.qrc(&qrc_file);
cc_builder_whole_archive.file(qtbuild.qrc(&qrc_file));
cc_builder_whole_archive_files_added = true;
}
if cc_builder_whole_archive_files_added {
cc_builder_whole_archive.compile("qt-static-initializers");
}

self.cc_builder.compile("cxx-qt-gen");
}
}
27 changes: 26 additions & 1 deletion crates/cxx-qt-gen/src/generator/cpp/qobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,31 @@ impl GeneratedCppQObjectBlocks {
self.metaobjects.append(&mut other.metaobjects);
self.methods.append(&mut other.methods);
}

pub fn from(qobject: &ParsedQObject) -> GeneratedCppQObjectBlocks {
let mut qml_specifiers = Vec::new();
if let Some(qml_metadata) = &qobject.qml_metadata {
// Somehow moc doesn't include the info in metatypes.json that qmltyperegistrar needs
// when using the QML_ELEMENT/QML_NAMED_ELEMENT macros, but moc works when using what
// those macros expand to.
qml_specifiers.push(format!(
"Q_CLASSINFO(\"QML.Element\", \"{}\")",
qml_metadata.name
));
// TODO untested
if qml_metadata.uncreatable {
qml_specifiers.push("Q_CLASSINFO(\"QML.Creatable\", \"false\")".to_owned());
}
// TODO untested
if qml_metadata.singleton {
qml_specifiers.push("QML_SINGLETON".to_owned());
}
}
GeneratedCppQObjectBlocks {
metaobjects: qml_specifiers,
..Default::default()
}
}
}

#[derive(Default)]
Expand Down Expand Up @@ -61,7 +86,7 @@ impl GeneratedCppQObject {
.base_class
.clone()
.unwrap_or_else(|| "QObject".to_string()),
..Default::default()
blocks: GeneratedCppQObjectBlocks::from(qobject),
};

// Generate methods for the properties, invokables, signals
Expand Down
2 changes: 1 addition & 1 deletion crates/cxx-qt-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub use generator::{
cpp::{fragment::CppFragment, GeneratedCppBlocks},
rust::GeneratedRustBlocks,
};
pub use parser::Parser;
pub use parser::{qobject::QmlElementMetadata, Parser};
pub use syntax::{parse_qt_file, CxxQtItem};
pub use writer::{cpp::write_cpp, rust::write_rust};

Expand Down
Loading

0 comments on commit 10e8ba8

Please sign in to comment.