Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tutorial on adding Rust wrappers for Qt C++ API #171

Merged
merged 2 commits into from
Jul 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 161 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,26 +108,178 @@ Enables `QtWebEngine` functionality. For more details see the [example](./exampl

This feature is disabled by default.

## What if a binding for the Qt C++ API you want to use is missing?
## 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.
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.
In this case, it is always possible to access C++ directly from your rust code
using the `cpp!` macro.

Example: from [`examples/graph/src/main.rs`](./examples/graph/src/main.rs#L37), the struct `Graph` is a `QObject` deriving from `QQuickItem`,
`QQuickItem::setFlag` is currently not exposed in the API but we wish to call it anyway.
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_:

1. Add `qttypes` to dependencies.
Likely, you would just stick to recent versions published on [crates.io](versions).
```toml
[dependencies]
qttypes = { version = "0.2", features = [ "qtquick" ] }
```
Add more Qt modules you need to the features array.
Refer to [qttypes crate documentation](docs.qttypes) for a full list of supported modules.
<br/>
If you _absolutely need_ latest unreleased changes, use this instead of `version = "..."`:
* `path = "../path/to/qmetaobject-rs/qttypes"` or
* `git = "https://github.com/woboq/qmetaobject-rs"`

2. Add `cpp` to dependencies and `cpp_build` to build-dependencies.
You can find up-to-date instructions on [`cpp` documentation](https://docs.rs/cpp) page.
```toml
[dependencies]
cpp = "0.5"

[build-dependencies]
cpp_build = "0.5"
```

3. Copy _build.rs_ script from [_qmetaobject/build.rs_](./qmetaobject/build.rs).
It will run `cpp_build` against you package, using environment provided by
[_qttypes/build.rs_](./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 an
`unsafe` 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`](https://docs.rs/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_](./examples/graph) directory.

Before adding wrappers, we put relevant `#include` lines inside a `{{` double
curly braced `}}` macro:

```rust
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](#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.QObject) trait.

```rust
#[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:

```rust
impl Graph {
fn appendSample(&mut self, value: f64) {
// ...
let obj = self.get_cpp_object();
cpp!(unsafe [obj as "QQuickItem *"] { obj->setFlag(QQuickItem::ItemHasContents); });
cpp!(unsafe [obj as "QQuickItem *"] {
obj->setFlag(QQuickItem::ItemHasContents);
});
// ...
}
}
```

But ideally, we should wrap as much as possible so this would not be needed. You can request API
as a github issue, or contribute via a pull request.
Alternatively, we could add a proper method wrapper, and call it without `unsafe`:

```rust
#[repr(u32)]
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 to [`QVariant`] 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. 🙂

[versions]: https://crates.io/crates/qmetaobject/versions
[trait.QObject]: https://docs.rs/qmetaobject/latest/qmetaobject/trait.QObject.html
[`QQuickItem::setFlag`]: https://doc.qt.io/qt-5/qquickitem.html#setFlag
[`QObject::cpp_construct()`]: https://docs.rs/qmetaobject/latest/qmetaobject/trait.QObject.html#tymethod.cpp_construct
[`QObjectPinned`]: https://docs.rs/qmetaobject/latest/qmetaobject/struct.QObjectPinned.html
[`QVariant`]: https://docs.rs/qmetaobject/latest/qmetaobject/struct.QVariant.html
[docs.qttypes]: https://docs.rs/qttypes/latest/qttypes/#cargo-features
40 changes: 37 additions & 3 deletions examples/graph/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ use qmetaobject::scenegraph::*;

mod nodes;

cpp! {{
#include <QtQuick/QQuickItem>
}}

#[derive(Default, QObject)]
struct Graph {
base: qt_base_class!(trait QQuickItem),
Expand All @@ -27,14 +31,44 @@ struct Graph {
),
}

// Example of adding an enum wrapper.

/// Wrapper for [`QQuickItem::Flag`][enum] enum.
///
/// [enum]: https://doc.qt.io/qt-5/qquickitem.html#Flag-enum
#[allow(unused)]
#[repr(u32)]
enum QQuickItemFlag {
ItemClipsChildrenToShape = 0x01,
ItemAcceptsInputMethod = 0x02,
ItemIsFocusScope = 0x04,
ItemHasContents = 0x08,
ItemAcceptsDrops = 0x10,
}

impl Graph {
// Example of adding a method wrapper with wrapper-specific notice.

/// Wrapper for [`QQuickItem::setFlag(QQuickItem::Flag flag, bool enabled = true)`][method] method.
///
/// # Wrapper-specific behavior
///
/// The `enabled` argument is always set to true.
///
/// [method]: https://doc.qt.io/qt-5/qquickitem.html#setFlag
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.m_samples.push(value);
self.m_samplesChanged = true;
// FIXME! find a better way maybe
let obj = self.get_cpp_object();
assert!(!obj.is_null());
cpp!(unsafe [obj as "QQuickItem *"] { obj->setFlag(QQuickItem::ItemHasContents); });
self.set_flag(QQuickItemFlag::ItemHasContents);
(self as &dyn QQuickItem).update();
}
}
Expand Down
5 changes: 3 additions & 2 deletions examples/graph/src/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ qrc! {
// However, there is quite some API to expose.

cpp! {{
#include <QtQuick/QQuickItem>

#include "src/gridnode.cpp"
#include "src/linenode.cpp"
#include "src/noisynode.cpp"
#include "src/gridnode.cpp"
#include <QtQuick/QQuickItem>
}}

pub enum NoisyNode {}
Expand Down
14 changes: 12 additions & 2 deletions qttypes/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,18 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//! if Qt is not found. Otherwise, when not enabled, the build will continue, but any use of the classes will
//! panic at runtime
//! - **`chrono`**: enable the conversion between [`QDateTime`] related types and the types from the `chrono` crate.
//! - **`qtquick`**, **`qtwebengine`**, **`qtquickcontrols2`**, **`qtmultimedia`**, **`qtmultimediawidgets`**, **`qtsql`**, **`qttest`**: link against these Qt modules
//! -
//!
//! Link against these Qt modules using cargo features:
//!
//! | Cargo feature | Qt module |
//! | ------------------------- | --------------------- |
//! | **`qtmultimedia`** | Qt Multimedia |
//! | **`qtmultimediawidgets`** | Qt Multimedia Widgets |
//! | **`qtquick`** | Qt Quick |
//! | **`qtquickcontrols2`** | Qt Quick Controls |
//! | **`qtsql`** | Qt SQL |
//! | **`qttest`** | Qt Test |
//! | **`qtwebengine`** | Qt WebEngine |
//!

#![cfg_attr(no_qt, allow(unused))]
Expand Down