Integrating QML and Rust: Creating a QMetaObject at Compile Time
In this blog post, I would like to present a research project I have been working on: Trying to use QML from Rust, and in general, using a C++ library from Rust.
The project is a Rust crate which allows to create QMetaObject at compile time from pure Rust code. It is available here: https://github.com/woboq/qmetaobject-rs
Qt and Rust
There were already numerous existing projects that attempt to integrate Qt and Rust. A great GUI toolkit should be working with a great language.
As far back as 2014, the project cxx2rust tried to generate automatic
bindings to C++, and in particular to Qt5.
The blog post explain all the problems.
Another project that automatically generate C++ bindings for Qt is
cpp_to_rust.
I would not pursue this way of automatically create bindings because it cannot produce a binding that can be used from idiomatic Rust code, without using unsafe
.
There is also qmlrs. The idea here is to
develop manually a small wrapper C++ library that exposes extern "C"
functions.
Then a Rust crate with a good and safe API can internally call these wrappers.
Similarly, the project qml-rust does approximately
the same, but uses the DOtherSide bindings as
the Qt wrapper library. The same used for D and Nim bindings for QML.
These two projects only concentrate on QML and not QtWidget nor the whole of Qt.
Since the API is then much smaller, this simplifies a lot the fastidious work of
creating the bindings manually.
Both these projects generate a QMetaObject at runtime from information given by rust macros.
Also you cannot use any type as parameter for your property or method arguments. You are limited
to convert to built-in types.
Finally, there is Jos van den Oever's
Rust Qt Binding Generator.
To use this project, one has to write a JSON description of the interface one wants to expose,
then the generator will generate the rust and C++ glue code so that you can easily call rust from
your Qt C++/Qml application.
What I think is a problem is that you are still expected to write some C++
and add an additional step in your build system.
That is perfectly fine if you want to add Rust to an existing C++ project, but not if you just
want a GUI for a Rust application.
Also writing this JSON description is a bit alien.
I started the qmetaobject crate mainly because I wanted to create the
QMetaObject at rust compile time.
The QMetaObject is a data structure which contains all the information about a
class deriving from QObject (or Q_GADGET
) so the Qt runtime can connect signals with
slots, or read and write properties. Normally, the QMetaObject is built at compile time from a
C++ file generated by moc
, Qt's meta object compiler.
I'm a fan of creating QMetaObject: I am contributing to Qt, and I also wrote
moc-ng and Verdigris which are all about creating QMetaObject.
Verdigris uses the power of C++ constexpr to create the QMetaObject at compile time, and I wanted
to try using Rust to see if it could also be done at compile time.
The qmetaobject
crate
The crate uses a custom derive macro to generate the QMetaObject.
Custom derive works by adding an annotation in front of a rust struct such as
#[derive(QObject)]
or #[derive(QGadget)]
.
Upon seeing this annotation, the rustc compiler will call the function from the qmetaobject_impl crate
which implements the custom derive. The function has the signature
fn(input : TokenStream) -> TokenStream
. It will be called at compile time, and takes
as input the source code of the struct it derives and should generate more source code
that will then be compiled.
What we do in this custom derive macro is first to parse the content of the struct and find about
some annotations. I've used a set of macros such as qt_property!
, qt_method!
and so on, similar to Qt's C++ macros. I could also have used custom attributes but I chose macros
as it seemed more natural coming from the Qt world (but perhaps this should be revised).
Let's simply go over a dummy example of using the crate.
extern crate qmetaobject; use qmetaobject::*; // For simplicity // Deriving from QObject will automatically implement the QObject trait and // generates QMetaObject through the custom derive macro. // This is equivalent to add the Q_OBJECT in Qt code. #[derive(QObject,Default)] struct Greeter { // We need to specify a C++ base class. This is done by specifying a // QObject-like trait. Here we can specify other QObject-like traits such // as QAbstractListModel or QQmlExtensionPlugin. // The 'base' field is in fact a pointer to the C++ QObject. base : qt_base_class!(trait QObject), // We declare the 'name' property using the qt_property! macro. name : qt_property!(QString; NOTIFY name_changed), // We declare a signal. The custom derive will automatically create // a function of the same name that can be called to emit it. name_changed : qt_signal!(), // We can also declare invokable methods. compute_greetings : qt_method!(fn compute_greetings(&self, verb : String) -> QString { return (verb + " " + &self.name.to_string()).into() }) } fn main() { // We then use qml_register_type as an equivalent to qmlRegisterType qml_register_type::<Greeter>(cstr!("Greeter"), 1, 0, cstr!("Greeter")); let mut engine = QmlEngine::new(); engine.load_data(r#" import QtQuick 2.6; import QtQuick.Window 2.0; import Greeter 1.0; Window { visible: true; // We can instantiate our rust object here. Greeter { id: greeter; name: 'World'; } // and use it by accessing its property or method. Text { text: greeter.compute_greetings('hello'); } }"#.into()); engine.exec(); }
In this example, we used qml_register_type
to register the type to QML, but we
can also also set properties on the global context. An example with this model, which also demonstrate
QGadget
// derive(QGadget) is the equivalent of Q_GADGET. #[derive(QGadget,Clone,Default)] struct Point { x: qt_property!(i32), y: qt_property!(i32), } #[derive(QObject, Default)] struct Model { // Here the C++ class will derive from QAbstractListModel base: qt_base_class!(trait QAbstractListModel), data: Vec<Point> } // But we still need to implement the QAbstractListModel manually impl QAbstractListModel for Model { fn row_count(&self) -> i32 { self.data.len() as i32 } fn data(&self, index: QModelIndex, role:i32) -> QVariant { if role != USER_ROLE { return QVariant::default(); } // We use the QGadget::to_qvariant function self.data.get(index.row() as usize).map(|x|x.to_qvariant()).unwrap_or_default() } fn role_names(&self) -> std::collections::HashMap<i32, QByteArray> { vec![(USER_ROLE, QByteArray::from("value"))].into_iter().collect() } } fn main() { let mut model = Model { data: vec![ Point{x:1,y:2} , Point{x:3, y:4} ], ..Default::default() }; let mut engine = QmlEngine::new(); // Registers _model as a context property. engine.set_object_property("_model".into(), &mut model); engine.load_data(r#" import QtQuick 2.6; import QtQuick.Window 2.0; Window { visible: true; ListView { anchors.fill: parent; model: _model; // We reference our Model object // And we can access the property or method of our gadget delegate: Text{ text: value.x + ','+value.y; } } }"#.into()); engine.exec();
Other implemented features include the creation of Qt plugins such as QQmlExtensionPlugin without writing a line of C++, only using rust and cargo. (See the qmlextensionplugins example.)
QMetaObject generation
The QMetaObject consists in a bunch of tables in the data section of the binary: a table of string and a table of integer. And there is also a function pointer with code used to read/write the properties or call the methods.
The custom derive macro will generate the tables as &'static[u8]
.
The moc generated code contains QByteArrayData
, built in C++, but since
we don't want to use a C++ compiler to generate the QMetaObject, we have to layout all the
bytes of the QByteArrayData
one by one. Another tricky part is the creation
of the Qt binary JSON for the plugin metadata. The Qt binary JSON is also an undocumented
data structure which needs to be built byte by byte, respecting many invariants such as alignment
and order of the fields.
The code from the static_metacall is just an extern "C" fn
. Then we can assemble all these
pointers in a QMetaObject. We cannot create const static structure containing pointers.
This is then implemented using the lazy_static!
macro.
QObject Creation
Qt needs a QObject*
pointer for our object.
It has virtual methods to get the QMetaObject.
The same applies for QAbstractListModel or any other class we could like to inherit from, which has
many virtual methods that we wish to override.
We will then have to materialize an actual C++ object on the heap.
This C++ counterpart is created by some of the C++ glue code. We will store a pointer to this C++
counterpart in the field annotated with the qt_base_class!
macro.
The glue code will instantiate a
RustObject<QObject>
. It is a class that inherits
from QObject (or any other QObject derivative) and overrides the virtual to forward
them to a callback in rust which will then be able to call the right function on the rust object.
One of the big problems is that in rust, contrary to C++, objects can be moved in memory at will.
This is a big problem, as the C++ object contains a pointer to the rust object. So the rust object
needs somehow to be fixed in memory. This can be achieved by putting it into a Box
or a Rc
, but even then, it is still possible to move the object in safe code.
This problem is not entirely fixed, but the interface takes the object by value and moves it to an
immutable location. Then the object can still be accessed safely from a QJSValue
object.
Note that QGadget does not need a C++ counter-part.
C++ Glue code
For this project I need a bit of C++ glue code to create the C++ counter part of my object, or
to access the C++ API for Qt types or QML API.
I am using the cpp!
macro from the cpp crate.
This macro allows embedding C++ code directly into rust code with very little boiler plate compared
to manually creating callbacks and declaring extern "C"
functions.
I even contributed a cpp_class macro
which allows wrapping C++ classes from rust.
Should an API be missing, it is easy to add the missing wrapper function. Also when we want to inherit from a class, we just need to imitate what is done for QAbstractListView, that is override all the virtual functions we want to override, and forward them to the function from the trait.
Final Words
My main goal with this crate was to try to see if we can integrate QML with idiomatic
and safe Rust code. Without requiring to use of C++ or any other alien tool for the developer.
I also had performance in mind and wanted to create the QMetaObject at compile time and
limit the amount of conversions or heap allocations.
Although there are still some problems to solve, and that the exposed API is far from complete,
this is already a beginning.
You can get the metaobject crate at this URL: https://github.com/woboq/qmetaobject-rs
Woboq is a software company that specializes in development and consulting around Qt and C++. Hire us!
If you like this blog and want to read similar articles, consider subscribing via our RSS feed (Via Google Feedburner, Privacy Policy), by e-mail (Via Google Feedburner, Privacy Policy) or follow us on twitter or add us on G+.
Article posted by Olivier Goffart on 07 June 2018
Click to subscribe via RSS or e-mail on Google Feedburner. (external service).
Click for the privacy policy of Google Feedburner.
Google Analytics Tracking Opt-Out
Loading comments embeds an external widget from disqus.com.
Check disqus privacy policy for more information.