This content originally appeared on DEV Community and was authored by Ayush
Introduction
Now that we have a working QML application start point, I would like to start working on my application. I will be using KDE Kirigami to create my application. While using Kirigami doesn't seem to require any extra configuration(at least not initially), the KI18n framework used for localization in KDE does need some work.
This post can serve as a guide to using C++ from Rust and creating stuff to use qt from Rust.
Create a Libray
I just created a library create using cargo:
cargo new ki18n-rs --lib
Rust has the convention to name the minimal wrappers of C libraries as *-sys
. However, this doesn't seem to be a convention in C++ libraries. Also, this crate isn't a minimal wrapper in any way.
Also, check that everything is working by running the test:
cargo test
Add Dependencies
Normal Dependencies
We will need to add a few dependencies to Cargo.toml
before starting:
[dependencies]
cpp = "0.5"
qttypes = "0.2"
qmetaobject = "0.2"
We need qmetaobject as a dependency since we will need QObject
and QmlEngine
later, which are not defined in qttypes. Honestly, we probably can avoid specifying qttypes, but I just left it there for now.
Build Dependencies
We also need a few dependencies for our build.rs
. Here is documentation covering the basics of build scripts. If I am being honest, I still don't completely understand everything about linking and other things which seem to be needed when interfacing with C/C++.
[build-dependencies]
cpp_build = "0.5"
semver = "1.0"
Writing build.rs
Start point
This is probably the portion that I found the most difficult. The README of qmetaobject-rs gives us a basic idea of the build script, so I started with that. Here is my starting script
use semver::Version;
fn main() {
eprintln!("cargo:warning={:?}", std::env::vars().collect::<Vec<_>>());
let qt_include_path = std::env::var("DEP_QT_INCLUDE_PATH").unwrap();
let qt_library_path = std::env::var("DEP_QT_LIBRARY_PATH").unwrap();
let qt_version = std::env::var("DEP_QT_VERSION")
.unwrap()
.parse::<Version>()
.expect("Parsing Qt version failed");
let mut config = cpp_build::Config::new();
if cfg!(target_os = "macos") {
config.flag("-F");
config.flag(&qt_library_path);
}
if qt_version >= Version::new(6, 0, 0) {
config.flag_if_supported("-std=c++17");
config.flag_if_supported("/std:c++17");
config.flag_if_supported("/Zc:__cplusplus");
}
config.include(&qt_include_path).build("src/lib.rs");
for minor in 7..=15 {
if qt_version >= Version::new(5, minor, 0) {
println!("cargo:rustc-cfg=qt_{}_{}", 5, minor);
}
}
let mut minor = 0;
while qt_version >= Version::new(6, minor, 0) {
println!("cargo:rustc-cfg=qt_{}_{}", 6, minor);
minor += 1;
}
}
Setting up KI18n
For linking KI18n, I decided to create a new function. This was my first time writing a build script, so honestly, I was pretty clueless. I currently have the include
path hardcoded because I couldn't find a better way to locate the header files. If anyone has suggestions, they are welcome to open issue at github or they can comment a solution.
fn ki18n_setup(config: &mut cpp_build::Config) {
let kf5_i18n_path = "/usr/include/KF5/KI18n";
config.include(kf5_i18n_path);
println!("cargo:rustc-link-lib=KF5I18n");
}
Tidying up Qt section
I also extracted the Qt section to it's seperate function to keep things tidy. Here is the new function:
fn qt_setup(config: &mut cpp_build::Config) -> Version {
let qt_include_path = std::env::var("DEP_QT_INCLUDE_PATH").unwrap();
let qt_library_path = std::env::var("DEP_QT_LIBRARY_PATH").unwrap();
let qt_version = std::env::var("DEP_QT_VERSION")
.unwrap()
.parse::<Version>()
.expect("Parsing Qt version failed");
if cfg!(target_os = "macos") {
config.flag("-F");
config.flag(&qt_library_path);
}
if qt_version >= Version::new(6, 0, 0) {
config.flag_if_supported("-std=c++17");
config.flag_if_supported("/std:c++17");
config.flag_if_supported("/Zc:__cplusplus");
}
config.include(&qt_include_path);
// Include qtcore
config.include(&format!("{}/{}", qt_include_path, "QtCore"));
qt_version
}
I had to include QtCore separately, and I don't currently have any solution for this. It seems the KLocalized
header file imports QObject
directly rather than using a relative path like include <QtCore/QObject>
. So this is probably a quick and dirty fix for now.
Here is my full build script:
use semver::Version;
fn main() {
eprintln!("cargo:warning={:?}", std::env::vars().collect::<Vec<_>>());
let mut config = cpp_build::Config::new();
let qt_version = qt_setup(&mut config);
ki18n_setup(&mut config);
config.build("src/lib.rs");
for minor in 7..=15 {
if qt_version >= Version::new(5, minor, 0) {
println!("cargo:rustc-cfg=qt_{}_{}", 5, minor);
}
}
let mut minor = 0;
while qt_version >= Version::new(6, minor, 0) {
println!("cargo:rustc-cfg=qt_{}_{}", 6, minor);
minor += 1;
}
}
fn qt_setup(config: &mut cpp_build::Config) -> Version {
let qt_include_path = std::env::var("DEP_QT_INCLUDE_PATH").unwrap();
let qt_library_path = std::env::var("DEP_QT_LIBRARY_PATH").unwrap();
let qt_version = std::env::var("DEP_QT_VERSION")
.unwrap()
.parse::<Version>()
.expect("Parsing Qt version failed");
if cfg!(target_os = "macos") {
config.flag("-F");
config.flag(&qt_library_path);
}
if qt_version >= Version::new(6, 0, 0) {
config.flag_if_supported("-std=c++17");
config.flag_if_supported("/std:c++17");
config.flag_if_supported("/Zc:__cplusplus");
}
config.include(&qt_include_path);
// Include qtcore
config.include(&format!("{}/{}", qt_include_path, "QtCore"));
qt_version
}
fn ki18n_setup(config: &mut cpp_build::Config) {
let kf5_i18n_path = "/usr/include/KF5/KI18n";
config.include(kf5_i18n_path);
println!("cargo:rustc-link-lib=KF5I18n");
}
Writing the Library
Now we can finally work on using KI18n from Rust. The cpp documentation is pretty great, and I would advise everyone to go through it if they are doing anything with C++ and Rust.
Creating Wrapper for KLocalizedContext
We cannot directly use KLocalizedContext from Rust since it is not a relocatable struct. Thus we need to create a wrapper struct which contains a unique_ptr to our actual KLocalizedContext. At least that's how qmetaobject-rs seems to get around the problem. Here's how the wrapper looks like:
cpp! {{
#include <KLocalizedContext>
#include <QtCore/QObject>
#include <QtQml/QQmlEngine>
#include <QtQuick/QtQuick>
struct KLocalizedContextHolder {
std::unique_ptr<KLocalizedContext> klocalized;
KLocalizedContextHolder(QObject *parent) : klocalized(new KLocalizedContext(parent)) {}
};
}}
We use the cpp! macro to include all the headers and define the struct.
Define Rust Struct
We now need to define a rust struct for KLocalizedContext, which refers to our Holder struct in C++. We use the cpp_class! macro for this:
cpp_class!(pub unsafe struct KLocalizedContext as "KLocalizedContextHolder");
The as "datatype"
part represents the C++ type our rust type/data refers to.
Implement members
Finally we can now implement the methods we want on this struct. Currently, I just want to register KLocalizedContext so that I can use the methods like i18n
from QML. So the implementation is given below:
impl KLocalizedContext {
pub fn init_from_engine(engine: &QmlEngine) {
let engine_ptr = engine.cpp_ptr();
cpp!(unsafe [engine_ptr as "QQmlEngine*"] {
engine_ptr->rootContext()->setContextObject(new KLocalizedContext(engine_ptr));
});
}
}
We have to use a closure in the cpp! macro to execute the instructions we want to perform.
Example Usage
Using this is pretty straightforward. We need to initialize KLocalizedContext after creating the engine. Here is an example:
use cstr::cstr;
use qmetaobject::prelude::*;
use ki18n_rs::KLocalizedContext;
fn main() {
let mut engine = QmlEngine::new();
KLocalizedContext::init_from_engine(&engine);
engine.load_data(r#"
import QtQuick 2.6
import QtQuick.Controls 2.0 as Controls
import QtQuick.Layouts 1.2
import org.kde.kirigami 2.13 as Kirigami
// Base element, provides basic features needed for all kirigami applications
Kirigami.ApplicationWindow {
// ID provides unique identifier to reference this element
id: root
// Window title
// i18nc is useful for adding context for translators, also lets strings be changed for different languages
title: i18nc("@title:window", "Hello World")
// Initial page to be loaded on app load
pageStack.initialPage: Kirigami.Page {
Controls.Label {
// Center label horizontally and vertically within parent element
anchors.centerIn: parent
text: i18n("Hello World!")
}
}
}
"#.into());
engine.exec();
}
Conclusion
I have publised this crate on crates.io as ki18n-rs. I will be exposing more of the C++ API when I get the time. However, since I haven't used KI18n in the past, pull requests and issues on Github will be extremely valuable. If possible, I would like to make the usage of KI18n from Rust as painless as possible.
This content originally appeared on DEV Community and was authored by Ayush
Ayush | Sciencx (2021-11-06T17:54:07+00:00) Using KI18n with Rust and Qml. Retrieved from https://www.scien.cx/2021/11/06/using-ki18n-with-rust-and-qml/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.