18 releases
0.2.10 | Nov 3, 2023 |
---|---|
0.2.9 | Jun 17, 2023 |
0.2.7 | Feb 23, 2022 |
0.2.5 | Nov 19, 2021 |
0.0.1 | Jul 9, 2018 |
#27 in GUI
690 downloads per month
Used in 207 crates
(9 directly)
385KB
7K
SLoC
QMetaObject crate for Rust
A framework empowering everyone to create Qt/QML applications with Rust.
It does so by building QMetaObject
s at compile time, registering QML types (optionally via exposing QQmlExtensionPlugin
s) and providing idiomatic wrappers.
Objectives
- Rust procedural macro (custom derive) to generate a
QMetaObject
at compile time. - Bindings for the main Qt types using the
cpp!
macro from thecpp
crate. - Users of this crate should not require to type any line of C++ or use another build system beyond cargo.
- Performance: Avoid any unnecessary conversion or heap allocation.
Presentation Blog Post: https://woboq.com/blog/qmetaobject-from-rust.html
Overview
use cstr::cstr;
use qmetaobject::prelude::*;
// The `QObject` custom derive macro allows to expose a class to Qt and QML
#[derive(QObject, Default)]
struct Greeter {
// Specify the base class with the qt_base_class macro
base: qt_base_class!(trait QObject),
// Declare `name` as a property usable from Qt
name: qt_property!(QString; NOTIFY name_changed),
// Declare a signal
name_changed: qt_signal!(),
// And even a slot
compute_greetings: qt_method!(fn compute_greetings(&self, verb: String) -> QString {
format!("{} {}", verb, self.name.to_string()).into()
})
}
fn main() {
// Register the `Greeter` struct to QML
qml_register_type::<Greeter>(cstr!("Greeter"), 1, 0, cstr!("Greeter"));
// Create a QML engine from rust
let mut engine = QmlEngine::new();
// (Here the QML code is inline, but one can also load from a file)
engine.load_data(r#"
import QtQuick 2.6
import QtQuick.Window 2.0
// Import our Rust classes
import Greeter 1.0
Window {
visible: true
// Instantiate the rust struct
Greeter {
id: greeter;
// Set a property
name: "World"
}
Text {
anchors.centerIn: parent
// Call a method
text: greeter.compute_greetings("hello")
}
}
"#.into());
engine.exec();
}
Features
- Create object inheriting from QObject, QQuickItem, QAbstractListModel, QQmlExtensionPlugin, ...
- Export Qt properties, signals, methods, ...
- Also support
#[derive(QGadget)]
(same as Q_GADGET) - Create Qt plugin (see examples/qmlextensionplugins)
- Partial scene graph support
Requires Qt >= 5.8
Cargo features
Cargo provides a way to enable (or disable default) optional features.
log
By default, Qt's logging system is not initialized, and messages from e.g. QML's console.log
don't go anywhere.
The "log" feature enables integration with log
crate, the Rust logging facade.
The feature is enabled by default. To activate it, execute the following code as early as possible in main()
:
fn main() {
qmetaobject::log::init_qt_to_rust();
// don't forget to set up env_logger or any other logging backend.
}
chrono_qdatetime
Enables interoperability of QDate
and QTime
with Rust chrono
package.
This feature is disabled by default.
webengine
Enables QtWebEngine
functionality. For more details see the example.
This feature is disabled by default.
What if a wrapper for the Qt C++ API is missing?
It is quite likely that you would like to call a particular Qt function which is not wrapped by this crate.
In this case, it is always possible to access C++ directly from your rust code
using the cpp!
macro.
We strive to increase coverage of wrapped API, so whenever there is something you need but currently missing, you are welcome to open a feature request on GitHub issues or send a Pull Request right away.
Tutorial: Adding Rust wrappers for Qt C++ API
This section teaches how to make your own crate with new Qt wrappers, and walk through a Graph example provided with this repository.
First things first, set up your Cargo.toml and build.rs:
-
Add
qttypes
to dependencies. Likely, you would just stick to recent versions published on crates.io.[dependencies] qttypes = { version = "0.2", features = [ "qtquick" ] }
Add more Qt modules you need to the features array. Refer to qttypes crate documentation for a full list of supported modules.
If you absolutely need latest unreleased changes, use this instead ofversion = "..."
:path = "../path/to/qmetaobject-rs/qttypes"
orgit = "https://github.com/woboq/qmetaobject-rs"
-
Add
cpp
to dependencies andcpp_build
to build-dependencies. You can find up-to-date instructions oncpp
documentation page.[dependencies] cpp = "0.5" [build-dependencies] cpp_build = "0.5"
-
Copy build.rs script from qmetaobject/build.rs. It will run
cpp_build
against you package, using environment provided by qttypes/build.rs.
Now, every time you build your package, content of cpp!
macros will be
collected in one big C++ file and compiled into a static library which will
later be linked into a final binary. You can find this cpp_closures.cpp
file buried inside Cargo target directory. Understanding its content might be
useful for troubleshooting.
There are two forms of cpp!
macro.
-
The one with double curly
{{
braces}}
appends its content verbatim to the C++ file. Use it to#include
headers, define C++ structs & classes etc. -
The other one is for calling expressions at runtime. It is usually written with
(
parenthesis)
, it takes[
arguments]
list and requires anunsafe
marker (either surrounding block or as a first keyword inside).
Order of macros invocations is preserved on a per-file (Rust module) basis;
but processing order of files is not guaranteed by the order of mod
declarations. So don't assume visibility — make sure to #include
everything
needed on top of every Rust module.
Check out documentation of cpp
to read more about how
it works internally.
Now that we are all set, let's take a look at the Graph example's code. It is located in examples/graph directory.
Before adding wrappers, we put relevant #include
lines inside a {{
double
curly braced }}
macro:
cpp! {{
#include <QtQuick/QQuickItem>
}}
If you need to include you own local C++ headers, you can do that too! Check out how main qmetaobject crate includes qmetaobject_rust.hpp header in every Rust module that needs it.
Next, we declare a custom QObject, just like in the overview, but
this time it derives from QQuickItem
. Despite its name, #[derive(QObject)]
proc-macro can work with more than one base class, as long as it is properly
wrapped and implements the QObject
trait.
#[derive(Default, QObject)]
struct Graph {
base: qt_base_class!(trait QQuickItem),
// ...
}
We wish to call QQuickItem::setFlag
method which is currently not
exposed in the qmetaobject-rs API, so let's call it directly:
impl Graph {
fn appendSample(&mut self, value: f64) {
// ...
let obj = self.get_cpp_object();
cpp!(unsafe [obj as "QQuickItem *"] {
obj->setFlag(QQuickItem::ItemHasContents);
});
// ...
}
}
Alternatively, we could add a proper method wrapper, and call it without unsafe
:
#[repr(C)]
enum QQuickItemFlag {
ItemClipsChildrenToShape = 0x01,
ItemAcceptsInputMethod = 0x02,
ItemIsFocusScope = 0x04,
ItemHasContents = 0x08,
ItemAcceptsDrops = 0x10,
}
impl Graph {
fn set_flag(&mut self, flag: QQuickItemFlag) {
let obj = self.get_cpp_object();
assert!(!obj.is_null());
cpp!(unsafe [obj as "QQuickItem *", flag as "QQuickItem::Flag"] {
obj->setFlag(flag);
});
}
fn appendSample(&mut self, value: f64) {
// ...
self.set_flag(QQuickItemFlag::ItemHasContents);
// ...
}
}
Note that C++ method takes optional second argument, but since optional
arguments are not supported by Rust nor by FFI glue, it is always left out
(and defaults to true
) in this case. To improve on this situation, we could
have added second required argument to Rust function, or implement
two "overloads" with slightly different names, e.g. set_flag(Flag, bool)
&
set_flag_on(Flag)
or enable_flag(Flag)
etc.
Assert for not-null should not be needed if object is guaranteed to be properly instantiated and initialized before usage. This applies to the following situations:
-
Call
QObject::cpp_construct()
directly and store the result in immovable memory location; -
Construct
QObjectPinned
instance: any access to pinned object or conversion toQVariant
ensures creation of C++ object; -
Instantiate object as a QML component. They are always properly default-initialized by a QML engine before setting any properties or calling any signals/slots.
And that's it! You have just implemented a new wrapper for a Qt C++ class method. Now send us a Pull Request. 🙂
Comparison to Other Projects
The primary goal of this crate is to provide idiomatic Rust bindings for QML. It focuses solely on QML, not QWidgets or any other non-graphical Qt API. The aim is to eliminate the need for users to know or use C++ and other build systems. This crate excels in achieving this goal.
-
CXX-Qt is an ideal solution for incorporating some Rust into an existing C++ project. CXX-Qt is newer than this crate and utilizes Rust features such as attribute macro, which didn't exist when the qmetaobject crate was designed. (Only derive procedural macro were available in stable Rust at that time)
-
The Rust Qt Binding Generator is another project that aids in integrating Rust logic into an existing C++/Qt project. This project was also developed before Rust had procedural macros, so it uses an external .json file to generate C++ and Rust code.
-
There are also several older crates that attempted to provide Rust binding around the Qt C++ API. Often automatically generated, these bindings are not idiomatic Rust, require unsafe code to use, and are no longer maintained.
-
Slint is a project created by the author of this crate. Slint is not a QML or Qt binding, but a new language inspired from QML, entirely implemented in Rust. It shares the same objective of providing a means to add a UI to a Rust project with idiomatic Rust API, but instead of using QML for the UI, it uses its own language.
The qmetaobject crate is currently only being passively maintained as focus has shifted towards developing Slint. You are encouraged to explore Slint as an exciting, innovative alternative for creating GUI in Rust projects.
Applications built with this crate
Dependencies
~1.9–3MB
~64K SLoC