2018-06-07

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.

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

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

Show more