commit 4e2265cb62c3ffb6edafb2fb3292bb7621de9c75 Author: SinuS Von SifriduS Date: Fri Apr 3 16:26:06 2026 +0200 rust/cxx-qt pocs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..6aab056 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[env] +QMAKE = "/home/sinus/DEV/Qt/6.11.0/gcc_64/bin/qmake" +QT_VERSION_MAJOR = "6" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5aca684 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +.settings +.project +.idea + +Cargo.lock +package-lock.json +/gen/ + +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Generated by Tauri +# will have schema files for capabilities auto-completion +/gen/schemas + + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.qmlls.ini +build_config.json diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..39bccfa --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,34 @@ +# file: poc-qt/Cargo.toml + +[workspace] +members = [ + "poc001", +] +resolver = "3" + +[workspace.package] +version = "0.1.0" +edition = "2024" +license = "MIT" +repository = "https://git.sasedev.com/Sasedev/poc-qt" +authors = ["SinuS von SifriduS "] + +[workspace.dependencies] +cxx = { version = "^1", features = ["c++14", "c++17", "c++20"] } +cxx-qt = { version = "^0.8", features = [] } +cxx-qt-build = { version = "^0.8", features = ["link_qt_object_files"] } +cxx-qt-lib = { version = "^0.8", features = ["full", "link_qt_object_files"] } +jsonschema = { version = "^0.40", features = [] } +serde = { version = "^1.0", features = ["derive"] } +serde_json = { version = "^1.0", features = [] } + +[profile.dev] +incremental = true # Compile your binary in smaller steps. + +[profile.release] +codegen-units = 1 # Allows LLVM to perform better optimization. +lto = true # Enables link-time-optimizations. +opt-level = 3 # s Prioritizes small binary size. Use `3` if you prefer speed. +panic = "abort" # Higher performance by disabling panic handlers. +strip = true # Ensures debug symbols are removed. + diff --git a/build_config.schema.json b/build_config.schema.json new file mode 100644 index 0000000..100bbd7 --- /dev/null +++ b/build_config.schema.json @@ -0,0 +1,82 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "active_profile", + "profiles" + ], + "additionalProperties": false, + "properties": { + "active_profile": { + "type": "string", + "minLength": 1 + }, + "profiles": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "object", + "required": [ + "qt", + "link" + ], + "additionalProperties": false, + "properties": { + "qt": { + "type": "object", + "required": [ + "qmake", + "lib_dir", + "qml_dir", + "plugins_dir", + "version_major" + ], + "additionalProperties": false, + "properties": { + "qmake": { + "type": "string", + "minLength": 1 + }, + "lib_dir": { + "type": "string", + "minLength": 1 + }, + "qml_dir": { + "type": "string", + "minLength": 1 + }, + "plugins_dir": { + "type": "string", + "minLength": 1 + }, + "version_major": { + "type": "integer", + "enum": [ + 5, + 6 + ] + } + } + }, + "link": { + "type": "object", + "required": [ + "rpaths" + ], + "additionalProperties": false, + "properties": { + "rpaths": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/poc001/Cargo.toml b/poc001/Cargo.toml new file mode 100644 index 0000000..a6506a2 --- /dev/null +++ b/poc001/Cargo.toml @@ -0,0 +1,20 @@ +# file: poc-qt/poc001/Cargo.toml + +[package] +name = "poc-qt001" +version = "0.1.0" +edition.workspace = true +license.workspace = true +authors.workspace = true + +[build-dependencies] +cxx-qt-build = { workspace = true, features = ["link_qt_object_files"] } +jsonschema = { workspace = true, features = [] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true, features = [] } + +[dependencies] +cxx = { workspace = true, features = [] } +cxx-qt = { workspace = true, features = [] } +cxx-qt-lib = { workspace = true, features = ["full", "link_qt_object_files"] } + diff --git a/poc001/build.rs b/poc001/build.rs new file mode 100644 index 0000000..eaced36 --- /dev/null +++ b/poc001/build.rs @@ -0,0 +1,297 @@ +// file: poc-qt/poc001/build.rs + +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; + +use cxx_qt_build::{CxxQtBuilder, QmlModule}; +use serde::Deserialize; +use serde_json::Value; + +const DEFAULT_CONFIG_STR: &str = include_str!("build_config.default.json"); +const CONFIG_SCHEMA_STR: &str = include_str!("../build_config.schema.json"); + +#[derive(Debug, Deserialize)] +struct BuildConfig { + active_profile: String, + profiles: BTreeMap, +} + +#[derive(Debug, Deserialize)] +struct BuildProfile { + qt: QtConfig, + link: LinkConfig, +} + +#[derive(Debug, Deserialize)] +struct QtConfig { + qmake: String, + lib_dir: String, + qml_dir: String, + plugins_dir: String, + version_major: u8, +} + +#[derive(Debug, Deserialize)] +struct LinkConfig { + rpaths: Vec, +} + +fn ensure_user_config_exists(config_path: &Path) { + if config_path.exists() { + return; + } + + match fs::write(config_path, DEFAULT_CONFIG_STR) { + Ok(()) => { + println!( + "cargo:warning=build_config.json was missing and has been created from build_config.default.json" + ); + } + Err(err) => { + panic!( + "failed to create {} from embedded defaults: {}", + config_path.display(), + err + ); + } + } +} + +fn validate_embedded_schema() { + let schema_json = parse_json_str(CONFIG_SCHEMA_STR, "embedded build_config.schema.json"); + + match jsonschema::meta::validate(&schema_json) { + Ok(()) => {} + Err(err) => { + panic!("embedded build_config.schema.json is not a valid JSON Schema: {err}"); + } + } +} + +fn validate_default_config() { + validate_config_str_against_schema(DEFAULT_CONFIG_STR, "embedded build_config.default.json"); +} + +fn validate_user_config(config_path: &Path) { + let raw = match fs::read_to_string(config_path) { + Ok(v) => v, + Err(err) => { + panic!("failed to read {}: {}", config_path.display(), err); + } + }; + + validate_config_str_against_schema(&raw, &format!("{}", config_path.display())); +} + +fn validate_config_str_against_schema(config_raw: &str, config_name: &str) { + let schema_json = parse_json_str(CONFIG_SCHEMA_STR, "embedded build_config.schema.json"); + let config_json = parse_json_str(config_raw, config_name); + + let validator = match jsonschema::validator_for(&schema_json) { + Ok(v) => v, + Err(err) => { + panic!( + "failed to build JSON Schema validator for {}: {}", + config_name, err + ); + } + }; + + if validator.is_valid(&config_json) { + return; + } + + let mut errors = Vec::new(); + for err in validator.iter_errors(&config_json) { + errors.push(format!( + "- {} at instance path {}", + err, + err.instance_path() + )); + } + + let joined = errors.join("\n"); + panic!("{} failed schema validation:\n{}", config_name, joined); +} + +fn load_build_config(config_path: &Path) -> BuildConfig { + let raw = match fs::read_to_string(config_path) { + Ok(v) => v, + Err(err) => { + panic!("failed to read {}: {}", config_path.display(), err); + } + }; + + match serde_json::from_str::(&raw) { + Ok(v) => v, + Err(err) => { + panic!( + "failed to deserialize {} into BuildConfig: {}", + config_path.display(), + err + ); + } + } +} + +fn resolve_profile_name(build_config: &BuildConfig) -> String { + let env_profile = std::env::var("BUILD_CONFIG_PROFILE"); + match env_profile { + Ok(v) => { + if v.trim().is_empty() { + build_config.active_profile.clone() + } else { + v + } + } + Err(_) => build_config.active_profile.clone(), + } +} + +fn resolve_profile<'a>(build_config: &'a BuildConfig, profile_name: &str) -> &'a BuildProfile { + match build_config.profiles.get(profile_name) { + Some(v) => v, + None => { + let mut available = Vec::new(); + for key in build_config.profiles.keys() { + available.push(key.clone()); + } + panic!( + "active profile '{}' not found in build_config.json. available profiles: {}", + profile_name, + available.join(", ") + ); + } + } +} + +fn validate_profile_paths(profile: &BuildProfile, profile_name: &str) { + ensure_existing_file(&profile.qt.qmake, profile_name, "qt.qmake"); + ensure_existing_dir(&profile.qt.lib_dir, profile_name, "qt.lib_dir"); + ensure_existing_dir(&profile.qt.qml_dir, profile_name, "qt.qml_dir"); + ensure_existing_dir(&profile.qt.plugins_dir, profile_name, "qt.plugins_dir"); + + let mut index = 0usize; + while index < profile.link.rpaths.len() { + ensure_existing_dir( + &profile.link.rpaths[index], + profile_name, + &format!("link.rpaths[{index}]"), + ); + index += 1; + } +} + +fn ensure_existing_file(path_str: &str, profile_name: &str, field_name: &str) { + let path = Path::new(path_str); + if !path.exists() { + panic!( + "profile '{}' has invalid {}: path does not exist: {}", + profile_name, + field_name, + path.display() + ); + } + if !path.is_file() { + panic!( + "profile '{}' has invalid {}: path is not a file: {}", + profile_name, + field_name, + path.display() + ); + } +} + +fn ensure_existing_dir(path_str: &str, profile_name: &str, field_name: &str) { + let path = Path::new(path_str); + if !path.exists() { + panic!( + "profile '{}' has invalid {}: path does not exist: {}", + profile_name, + field_name, + path.display() + ); + } + if !path.is_dir() { + panic!( + "profile '{}' has invalid {}: path is not a directory: {}", + profile_name, + field_name, + path.display() + ); + } +} + +fn apply_build_environment(profile: &BuildProfile) { + unsafe { + std::env::set_var("QMAKE", &profile.qt.qmake); + std::env::set_var("QT_VERSION_MAJOR", profile.qt.version_major.to_string()); + } + + println!("cargo:rerun-if-env-changed=BUILD_CONFIG_PROFILE"); + println!("cargo:rerun-if-env-changed=QMAKE"); + println!("cargo:rerun-if-env-changed=QT_VERSION_MAJOR"); +} + +fn apply_linker_settings(profile: &BuildProfile) { + let mut index = 0usize; + while index < profile.link.rpaths.len() { + println!( + "cargo:rustc-link-arg=-Wl,-rpath,{}", + profile.link.rpaths[index] + ); + index += 1; + } +} + +fn emit_runtime_hints(profile: &BuildProfile, profile_name: &str) { + println!( + "cargo:warning=using build profile '{}' with qmake '{}'", + profile_name, profile.qt.qmake + ); + println!( + "cargo:warning=qt lib_dir='{}', qml_dir='{}', plugins_dir='{}'", + profile.qt.lib_dir, profile.qt.qml_dir, profile.qt.plugins_dir + ); +} + +fn parse_json_str(raw: &str, label: &str) -> Value { + match serde_json::from_str::(raw) { + Ok(v) => v, + Err(err) => { + panic!("failed to parse {} as JSON: {}", label, err); + } + } +} + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=build_config.default.json"); + println!("cargo:rerun-if-changed=build_config.schema.json"); + println!("cargo:rerun-if-changed=src/bridge.rs"); + println!("cargo:rerun-if-changed=qml/main.qml"); + + let config_path = PathBuf::from("build_config.json"); + + ensure_user_config_exists(&config_path); + validate_embedded_schema(); + validate_default_config(); + validate_user_config(&config_path); + + let build_config = load_build_config(&config_path); + let profile_name = resolve_profile_name(&build_config); + let profile = resolve_profile(&build_config, &profile_name); + + validate_profile_paths(profile, &profile_name); + apply_build_environment(profile); + apply_linker_settings(profile); + + CxxQtBuilder::new_qml_module(QmlModule::new("com.sasedev.hello").qml_files(["qml/main.qml"])) + .qt_module("Qml") + .qt_module("Network") + .files(["src/bridge.rs"]) + .build(); + + emit_runtime_hints(profile, &profile_name); +} diff --git a/poc001/build_config.default.json b/poc001/build_config.default.json new file mode 100644 index 0000000..6a31de4 --- /dev/null +++ b/poc001/build_config.default.json @@ -0,0 +1,33 @@ +{ + "active_profile": "sinus", + "profiles": { + "sinus": { + "qt": { + "qmake": "/home/sinus/DEV/Qt/6.11.0/gcc_64/bin/qmake", + "lib_dir": "/home/sinus/DEV/Qt/6.11.0/gcc_64/lib", + "qml_dir": "/home/sinus/DEV/Qt/6.11.0/gcc_64/qml", + "plugins_dir": "/home/sinus/DEV/Qt/6.11.0/gcc_64/plugins", + "version_major": 6 + }, + "link": { + "rpaths": [ + "/home/sinus/DEV/Qt/6.11.0/gcc_64/lib" + ] + } + }, + "system": { + "qt": { + "qmake": "/usr/bin/qmake6", + "lib_dir": "/usr/lib/x86_64-linux-gnu", + "qml_dir": "/usr/lib/x86_64-linux-gnu/qt6/qml", + "plugins_dir": "/usr/lib/x86_64-linux-gnu/qt6/plugins", + "version_major": 6 + }, + "link": { + "rpaths": [ + "/usr/lib/x86_64-linux-gnu" + ] + } + } + } +} \ No newline at end of file diff --git a/poc001/qml/main.qml b/poc001/qml/main.qml new file mode 100644 index 0000000..35c1e7e --- /dev/null +++ b/poc001/qml/main.qml @@ -0,0 +1,46 @@ +// file: poc-qt/poc001/qml/main.qml + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import com.sasedev.hello + +ApplicationWindow { + visible: true + width: 420 + height: 220 + title: "Rust + Qt Hello" + + Greeter { + id: greeter + message: "Hello" + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + TextField { + Layout.fillWidth: true + placeholderText: "Enter your name" + text: greeter.name + + onTextChanged: { + greeter.name = text + } + } + + Button { + text: "Say Hello" + onClicked: { + greeter.sayHello() + } + } + + Label { + Layout.fillWidth: true + text: greeter.message + } + } +} diff --git a/poc001/src/bridge.rs b/poc001/src/bridge.rs new file mode 100644 index 0000000..95de092 --- /dev/null +++ b/poc001/src/bridge.rs @@ -0,0 +1,53 @@ +// file: poc-qt/poc001/src/bridge.rs + +use core::pin::Pin; +use cxx_qt_lib::QString; + +#[derive(Default)] +pub struct GreeterRust { + name: QString, + message: QString, +} + +#[cxx_qt::bridge] +pub mod ffi { + unsafe extern "C++" { + include!("cxx-qt-lib/qstring.h"); + type QString = cxx_qt_lib::QString; + } + + extern "RustQt" { + #[qobject] + #[qml_element] + #[qproperty(QString, name)] + #[qproperty(QString, message)] + type Greeter = super::GreeterRust; + + #[qinvokable] + #[cxx_name = "sayHello"] + fn say_hello(self: Pin<&mut Greeter>); + } + + impl cxx_qt::Initialize for Greeter {} +} + +impl cxx_qt::Initialize for ffi::Greeter { + fn initialize(self: Pin<&mut Self>) { + self.set_message(QString::from("Hello")); + } +} + +impl ffi::Greeter { + pub fn say_hello(self: Pin<&mut Self>) { + let current_name = self.as_ref().name().to_string(); + let trimmed_name = current_name.trim(); + + let next_message = if trimmed_name.is_empty() { + QString::from("Hello") + } else { + QString::from(format!("Hello {}", trimmed_name)) + }; + + self.set_message(next_message); + } +} diff --git a/poc001/src/main.rs b/poc001/src/main.rs new file mode 100644 index 0000000..978d104 --- /dev/null +++ b/poc001/src/main.rs @@ -0,0 +1,24 @@ +// file: poc-qt/poc001/src/main.rs + +mod bridge; + +use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QUrl}; + +fn main() { + let mut app = QGuiApplication::new(); + let mut engine = QQmlApplicationEngine::new(); + + if let Some(engine_ref) = engine.as_mut() { + let url = QUrl::from("qrc:/qt/qml/com/sasedev/hello/qml/main.qml"); + engine_ref.load(&url); + } else { + eprintln!("failed to create QQmlApplicationEngine"); + return; + } + + if let Some(app_ref) = app.as_mut() { + app_ref.exec(); + } else { + eprintln!("failed to create QGuiApplication"); + } +}