White-Oak / qml-rust

QML (Qt Quick) bindings for Rust language
MIT License
205 stars 18 forks source link

Accessing QObjects from Rust threads #32

Open flanfly opened 7 years ago

flanfly commented 7 years ago

I want to modify a Qt model from another thread. Currently I define the model using the Q_LISTMODEL! macro, and call threaded() to start another thread. When calling append_row on the model while inside the new thread, the view does not get updated and the application prints the following:

QObject::connect: Cannot queue arguments of type 'QQmlChangeSet'
(Make sure 'QQmlChangeSet' is registered using qRegisterMetaType().)

Searching the web it seems that changing a Qt model must happen in the GUI thread [1].

Looking at the code of threaded I'm not sure whenever I even want to use that function. It simply copies a mutable pointer to the new thread. As the new and the original thread have now a mutable reference to the object, this creates a possible data race and eliminates Rusts safety guarantees:

pub struct Blah {
  value: usize,
}

Q_OBJECT!(
pub Blah as QBlah {
  signals:
  slots:
    fn boom();
  properties:
});

impl QBlah {
  fn boom(&mut self) -> Option<&QVariant> {
    self.threaded(|s| {
        let value1: &mut usize = &mut s.value;
    });
    let value2: &mut usize = &mut self.value;
    None
  }
}

Both value1 and value2 can exist concurrently.

1: http://stackoverflow.com/questions/30421176/no-update-in-listview and http://blog.hostilefork.com/qt-model-view-different-threads/

White-Oak commented 7 years ago

You can call slots and signals and maybe change properties of Qt objects from different threads, but yes, you cannot change GUI for example. That is a very tricky and complicated topic -- the easiest would be to forbid any concurrent usage of QObjects, but that would just cripple the possibilities to work with qml.

flanfly commented 7 years ago

Ok, how about we add a mechanism that allows us to run Rust code in the GUI thread? For example calling a user provided function every event loop iteration.

White-Oak commented 7 years ago

@flanfly well now that is quite an interesting idea...

flanfly commented 7 years ago

The easiest way would be to allow spinning the event loop manual as I suggested in #25. So you can do stuff like this.

let engine = QmlEngine::new();
// ...
loop {
  engine.loop();
  // do stuff in GUI thread
}

It's still rather inelegant and maybe wasteful. Seems like the canonical way is to use a QTimer with interval set to 0. For this we'd need to expose the QTimer interface in DOtherSide and qml-rust.

vandenoever commented 7 years ago

A common use-case is that the rust replaces the content in a QListModel that is shown with QML. I do not currently see an sample that shows how to do this. The existing examples populate the QListModel before the UI is shown and do not modify it afterwards.

Ideally the contents of the QListModel would be replaced in one go, so the user does not see the list being repopulated. Would it be most efficient (or even possible) to make the model a property and set that?

White-Oak commented 7 years ago

@vandenoever https://github.com/White-Oak/kefia does it. In response to actions, QML calls Rust slots that modify QModelList.

vandenoever commented 7 years ago

@White-Oak Thanks! I've now ported my rust+qml example code to qml-rust.

https://gitlab.com/vandenoever/metadatadb/commit/7d9efab964bd9859c44c8b9bda0b6bccc944e54a