From dceeb470601b6a4ab59c2b58af819dde001f15f5 Mon Sep 17 00:00:00 2001 From: ivan tkachenko Date: Sat, 10 Jul 2021 17:26:46 +0300 Subject: [PATCH 1/2] Add tutorial on adding Rust wrappers for Qt C++ API Adds set_flag as a separate method on Graph struct, to extend the tutorial with fully usable wrapper and wrapper-specific documentation. Fixes potential compilation issue of Graph example: cpp! with #include was missing from main.rs, and the C++ code using QQuickItem accidentally worked, because of #include declared in cpp! inside another Rust file/module. But cpp crate does not specify in which order content of cpp! content between files, so it should not be relied on. Reorders #include lines in Graph example, such that external (Qt) stuff is on top, and local imports follow below. Resolves #169 --- README.md | 165 ++++++++++++++++++++++++++++++++++-- examples/graph/src/main.rs | 40 ++++++++- examples/graph/src/nodes.rs | 5 +- 3 files changed, 196 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 7a5d04ef..fa43ccd9 100644 --- a/README.md +++ b/README.md @@ -108,26 +108,173 @@ 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. If you need latest unreleased changes, use + * `path = "../qttypes"` or + * `git = "https://github.com/woboq/qmetaobject-rs"` + otherwise, stick to recent versions published on [crates.io](versions). + ```toml + [dependencies] + qttypes = { version = "0.2", features = ["qtquick", "..."] } + ``` + +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 +}} +``` + +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 diff --git a/examples/graph/src/main.rs b/examples/graph/src/main.rs index 9271cc3e..8bec2049 100644 --- a/examples/graph/src/main.rs +++ b/examples/graph/src/main.rs @@ -9,6 +9,10 @@ use qmetaobject::scenegraph::*; mod nodes; +cpp! {{ + #include +}} + #[derive(Default, QObject)] struct Graph { base: qt_base_class!(trait QQuickItem), @@ -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(); } } diff --git a/examples/graph/src/nodes.rs b/examples/graph/src/nodes.rs index d65307bf..9657454b 100644 --- a/examples/graph/src/nodes.rs +++ b/examples/graph/src/nodes.rs @@ -21,10 +21,11 @@ qrc! { // However, there is quite some API to expose. cpp! {{ + #include + + #include "src/gridnode.cpp" #include "src/linenode.cpp" #include "src/noisynode.cpp" - #include "src/gridnode.cpp" - #include }} pub enum NoisyNode {} From 783f963324eb34140b6afbac04e9804019d23e75 Mon Sep 17 00:00:00 2001 From: ivan tkachenko Date: Mon, 12 Jul 2021 16:43:26 +0300 Subject: [PATCH 2/2] Fix concerns raised during review (#171) * Rewords dependencies section, to nudge reader toward using published versions of qttypes. * Changes local path to explicitly absurd, to avoid assumptions that we are working from qmetaobject-rs subdirectory. * Removes "..." from features array, to make this line copy-pastable. * Comments out ... in appendSample function to avoid breaking Rust syntax (and make it copy-pastable too). * Formats qttypes cargo features comma-separated list of Qt modules into a table. --- README.md | 19 ++++++++++++------- qttypes/src/lib.rs | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index fa43ccd9..234fae8b 100644 --- a/README.md +++ b/README.md @@ -127,14 +127,18 @@ through a Graph example provided with this repository. First things first, set up your _Cargo.toml_ and _build.rs_: -1. Add `qttypes` to dependencies. If you need latest unreleased changes, use - * `path = "../qttypes"` or - * `git = "https://github.com/woboq/qmetaobject-rs"` - otherwise, stick to recent versions published on [crates.io](versions). +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", "..."] } + 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. +
+ 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. @@ -241,9 +245,9 @@ impl Graph { } fn appendSample(&mut self, value: f64) { - ... + // ... self.set_flag(QQuickItemFlag::ItemHasContents); - ... + // ... } } ``` @@ -278,3 +282,4 @@ method. Now send us a Pull Request. 🙂 [`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 diff --git a/qttypes/src/lib.rs b/qttypes/src/lib.rs index 7474e778..c379ffda 100644 --- a/qttypes/src/lib.rs +++ b/qttypes/src/lib.rs @@ -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))]