Using KI18n with Rust and Qml
Tags: rust, qml
Published: 2021-11-06 17:54:07

## 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](https://develop.kde.org/frameworks/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 --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:
```toml
[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](https://doc.rust-lang.org/cargo/reference/build-scripts.html) 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++.
```toml
[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](https://github.com/woboq/qmetaobject-rs) gives us a basic idea of the build script, so I started with that.  Here is my starting script
```Rust
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.
```Rust
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:
```rust
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:
```Rust
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](https://docs.rs/cpp/0.5.6/cpp/) 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:
```rust
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:
```Rust
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:
```rust
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:
```Rust
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](crates.io) as [ki18n](https://crates.io/crates/ki18n). 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.