Rust-Qt alpha release and feedback request

I'd like to announce an alpha release of Rust-Qt crates and cpp_to_rust generator.

Repository and documentation

Qt is a highly popular C++ cross-platform framework and one of the very few fully functional classic GUI toolkits. I think bringing power and convenience of Qt to Rust applications would greatly improve its popularity.

Qt is very large, so I'm developing an automatic generator similar to bindgen. This project has grown far more complicated than I expected. It's very hard to make the bindings convenient, and making them idiomatic in Rust terms seems impossible. I always have a lot of ideas for improvement, but I decided to stop for a moment and ask the community for feedback.

The qt_core, qt_gui, qt_widgets and qt_ui_tools crates are functional now. In addition to documentation, there are some tests and examples to help you get started. There are a lot of issues and missing features, but it's good enough to make a small program and play with it. You can also try to run the generator yourself if you want. However, the project is far from ready for production, and a lot of things will change in the future.

If you're interested, I encourage you to take a look at my project and tell me what you think. What are the biggest issues? Do you notice something important missing? How would you design Qt API in Rust? Would you use Rust-Qt in your projects when it's ready? I'd be happy for any feedback. Contributions to the project are welcome, of course. Also feel free to ask any questions.

34 Likes

Can you provide some examples of what is not idiomatic?

For example, transfering a pointer to an object in Qt can mean either transfer of ownership or providing a reference, and the only way to distinguish between them is to read the documentation. So in order to express transfer of ownership properly in Rust, we need either to annotate each case manually or teach the generator to read the documentation (which is not as impossible as it sounds).

Another major complication is function overloading, which is heavily used in Qt but doesn't exist in Rust. The right Rust way is to name each function properly, and this is not possible to do automatically. Some sort of overloading can be achieved for functions with the same number of arguments. Current implementation uses tuples of arguments, e.g. func(a, b) becomes func((a, b)), so that all variants have 1 argument. This solution is the opposite of idiomatic but in general there is simply no better way.

1 Like

I find the ambiguity of the referent of "its" here amusing :slight_smile:

5 Likes

Cool! So we ended up switching off of a couple Rust QT platforms for performance reasons in https://github.com/das-labor/panopticon

The leader developer @flanfly knows (way) more about this than me, but iirc it was a constant problem, perhaps this crate adds something fresh here?

very cool!
Do you plan to support QML? :slight_smile:

I try to keep the overhead as low as possible. Each function call just performs some necessary type conversions and calls a C++ function through FFI, and that function also performs some type conversions and calls the original function. Most of conversions are zero-cost, so the most significant overhead is caused just by calling these two intermediate functions. It's also possible to use many of the types without additional heap allocations, so that shouldn't be an issue. I'm worried about performance of the compilers though. Building the crates requires quite a lot of resources.

I'm interested to know what kind of performance issues you ran into. Maybe I will do some benchmarking in the future.

Ideally I want to support all main Qt modules, but it's very hard to do it all at once. I prefer to focus on QtWidgets until the generator becomes good enough to handle all modules better.

2 Likes

Awesome work. Had a bit of trouble getting it to work on Windows but got it squared away moving around some dlls. Simple hello world here... just in time for the Windows subsystem fix to land in stable.

1 Like

Cool project! I wonder how you are going to support deriving QObjects and template specialization.

In Panopticon we share data w/ the frontend by subclassing QAbstractItemModel and filling it asynchronously. We use signals/slots w/ a QT_QUEUED_CONNECTION to add data to the model from a Rust thread. This involves converting plain C FFI data types into QObject subclasses and copying them to the main (GUI) thread (example).

Both deriving QObject (and QAbstactItemModel) as well as calling qRegisterMetaType() (to make the custom types copyable) is done directly in C++. It would be cool to do this w/o a C++ compiler.

1 Like

The main idea of how deriving should work is described here. It's not implemented yet, but definitely possible. Qt heavily relies on deriving, so it's definitely a high priority task.

The current implementation is not able to parse template specializations and handle such data. And even if it were, there are still cases of "hidden" template specialization, e.g. QVector<T>::startsWith is only available if T can be compared for equality. For now, only the main template class definition is considered, and all specializations must be specified manually.

In generated Rust API, each template instantation is a separate type, meaning you can't write code that is generic over Rust equivalents of QVector<T1> and QVector<T2>. This is partly due to template specialization: it's not possible to introduce a trait that represents API of a template class, because there can be template arguments for which some methods simply don't exist (e.g. QFuture<void> lacks some methods). I think current way is good enough. Template specialization is not very common in Qt.

It should become possible when subclassing API is implemented. If Panopticon used Rust-Qt it would be a very good example. But note that Rust-Qt still requires a C++ compiler to build.

1 Like

When I tried binding Qt way back I also opted for the tuple approach for parameter overloading. It might not be idiomatic in Rust, but I think that's quite ok since this is obvisouly a binding for a very-C++ framework. I wouldn't worry about it that much...

Btw., how are signals & slots? I remember them being a bit of pain point for me, esp. when I tried to make it possible to bind closures as signal handlers.

In any case, awesome job.

I have to agree. It does make the docs somewhat awkward, but I'm not sure there's a better solution, given the circumstances.

On the plus side, I applaud the compile-time-checkability of approximating overloaded function args by implementing an interface trait for various tuples.

(My main interest in Rust is as a maximally compile-time checked replacement for Python in the creation of I/O-bound applications... within the limitations imposed by "Not Haskell. It's too hard for me to reason about performance characteristics and/or memory footprint in a lazy, GCed, pure-functional language.")

I'm not the type to depend on alpha libraries (I have enough trouble finding time to maintain projects written against mature ones) but, once this has matured a bit, I definitely foresee myself using it to replace the approach I'm currently migrating to. (Write/rewrite as much of the backend as possible in Rust, expose a Python API using rust-cpython, and write/keep the frontend in PyQt.)

EDIT: One thing I do like about your decision to use traits to approximate function overloading is that, because you used public traits, libraries which depend on these bindings can implement them to get a function overloading analogue to Deref coercion.

someQtMethod(myCustomType)

I love this so much :smiley:

I made it possible to bind built-in signals to built-in slots or Rust closures. Binding to closures seems to be quite inconvenient now: you have to add a separate field in the struct to keep each slot alive, and the borrow checker doesn't really allow to capture what you usually want in closures. I'm going to work on this further and will appreciate any suggestions.

That's an interesting thought. However, I think using these traits in such way would confuse future readers of the code. And you can't do that for non-overloaded functions because there are no argument traits for them, so you can't do this trick consistently. The traits are public because you can't use private traits in public interface.

True, but it's still nice to know that, should I run into a situation where it actually does make the code clearer and more intuitive (some kind of hlist implementation, perhaps?), it's a possibility for at least part of the Qt API.

I had a random thought about ownership:
Why not use all the Qt types behind Rc pointers?
Yes there would be some overhead but I'm thinking it wouldn't be enough to make a difference in program GUI code.
And on the other hand, most (all?) ownership issues would be solved (assuming Qt doesn't create any cyclic pointers anywhere that's observable to the world).

For the same reason you can't do that in C++. Qt implicitly keeps pointers to some objects, and Qt internals don't use any kind of smart pointers widely, so you can't universally use smart pointers for all types. For example, you can create a QListWidgetItem and put it in a QListWidget, then call QListWidget::clear, and QListWidgetItem is deleted. That means you can't use RC for QListWidgetItem.

For QObject-based objects there is nice QPointer class. It's a pointer that automatically becomes null when the object is deleted. I definitely need to add Rust API for that, but I don't really want to use it everywhere by default because I want to follow the "you don't pay for what you don't use" principle.