rust/cxx-qt pocs

This commit is contained in:
2026-04-03 16:26:06 +02:00
commit 4e2265cb62
10 changed files with 636 additions and 0 deletions

3
.cargo/config.toml Normal file
View File

@@ -0,0 +1,3 @@
[env]
QMAKE = "/home/sinus/DEV/Qt/6.11.0/gcc_64/bin/qmake"
QT_VERSION_MAJOR = "6"

44
.gitignore vendored Normal file
View File

@@ -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

34
Cargo.toml Normal file
View File

@@ -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 <sinus@sasedev.net>"]
[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.

82
build_config.schema.json Normal file
View File

@@ -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
}
}
}
}
}
}
}
}
}

20
poc001/Cargo.toml Normal file
View File

@@ -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"] }

297
poc001/build.rs Normal file
View File

@@ -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<String, BuildProfile>,
}
#[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<String>,
}
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::<BuildConfig>(&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::<Value>(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);
}

View File

@@ -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"
]
}
}
}
}

46
poc001/qml/main.qml Normal file
View File

@@ -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
}
}
}

53
poc001/src/bridge.rs Normal file
View File

@@ -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);
}
}

24
poc001/src/main.rs Normal file
View File

@@ -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");
}
}