Closed emoon closed 4 years ago
Before generating code, we need to know which signals and slots do exist in each class. Clang already provides information about all methods, including their types and visibility, and we just need to figure out which of them are signals and slots. I think it's better to do it by searching for Q_SIGNALS, Q_SLOTS and other visibility tokens in the header and determining which methods are declared within signals and slots sections. IIRC, moc does it this way (I'll take a look at its source).
Some versions of QObject::connect
require text representation of the method. In C++, it's usually used with a macro, like SIGNAL(bytesWritten(qint64))
, but it's pretty much equivalent to "2bytesWritten(qint64)"
("1" means slot and "2" means signal). It turns out Qt is smart enough to recognize the method as "bytesWritten(long long)" (while qint64 is a define for "long long"), so there should be no significant problems in constructing text representation.
An alternative way is to use QMetaObject
to request information about signals and slots at runtime, during an additional step of parsing. It would be harder to make a mistake there, but it significantly complicates the process.
There are 3 general types of QObject::connect
:
There is no way to expose pointers to member functions through FFI because it involves C++ template magic and compile-time checks. The only way would be to generate connect statements for each available pairs of sender and receiver, and it's just not reasonable.
Connecting to a functor could probably be useful, but it would still require generation of the connect statement for each available sender. I would rather not do it if I don't have to. I think creating slots on Rust side will be as easy as connecting to functors in C++, so there is no point in implementing it.
So, we only need to support connect via text representation:
QMetaObject::Connection connect(const QObject* sender, const char* signal, const QObject* receiver, const char* method, Qt::ConnectionType type = Qt::AutoConnection)
Rust users will want to interact with Qt via signals and slots, but they will not need to use them for communication within their Rust applications. So, there is no need to declare custom types in Qt metatype system. There is no point in creating signals and slots with arguments that doesn't correspond to those encountered in Qt, because there wouldn't anything they could be connected to. Each Qt library has limited set of arguments, and we can generate specialized code to make signals and slots with these arguments available.
The easiest way I can see is to generate a QObject
subclass containing a signal or a slot for each list of arguments:
class QSignalWrapperQInt64 : public QObject {
Q_OBJECT
signals:
void method(qint64);
};
class QSlotWrapperQInt64 : public QObject {
Q_OBJECT
public:
QSlotWrapperQInt64(void (*func)(void*, qint64), void* data);
public slots:
void method(qint64);
private:
void (*m_func)(void*, qint64);
void* m_data;
};
QSlotWrapperQInt64::QSlotWrapperQInt64(void (*func)(void *, qint64), void *data) {
m_func = func;
m_data = data;
}
void QSlotWrapperQInt64::method(qint64 arg1) {
m_func(m_data, arg1);
}
The slot wrapper provides a callback parameter, and I believe it makes passing Rust closures as callbacks a lot easier. The Rust API will expose constructors of these classes and the method to send the signal (if it's a signal). The Rust struct will contain a CppBox with a QObject
pointer. C++ object will be deleted when the struct goes out of scope, ensuring automatic disconnect. Creating an object with signals and slots in Rust would be looking like this:
impl MyStruct {
pub fn new() -> MyStruct {
MyStruct {
my_data: 42,
my_signal1: qt_core::custom_signal::SignalI64::new(),
my_slot1: qt_core::custom_slot::SlotI64::new(|x| {
println!("received number: {}", x);
})
}
}
fn emit_example(&self) {
self.my_signal1.emit(42);
}
}
I suspect Rust will not be happy if you try to borrow something inside the closure. But it will be right. Using RefCell to make it happy is an acceptable solution in this case.
At first, I thought we can put all signals and slots into one class. But what if someone wants to use two different signals of the same type? They would still need to create multiple objects. Creating a QObject
for each signal or slot provides more flexible situation. And the Rust struct keeps ownership of the objects, so it can delete them all at once without any complications.
Slot wrappers should probably expose QObject::sender()
protected method. It's the only way to figure out where the signal came from in case of multiple connections. It can be done by creating another constructor that accepts closure with an additional argument. That argument will have the value of sender()
:
qt_core::custom_slot::SlotI64::new_with_sender(|sender, x| {
println!("received number: {} from {:?}", x, sender);
})
Slot wrappers should have invoke(...)
method to call the slot directly.
Like in PyQt, I would like to have signal and slot objects instead of raw QObject::connect
interface. Each object can provide QObject*
pointer to the object and const char*
representation of the method. Each signal or slot also should have a type parameter indicating argument types or the method, and Rust API will enforce that the types are compatible. A signal object will have a connect()
function that receives another signal or slot object and optionally a connection mode.
Rust's custom signal and slot wrapper structs will implement signal and slot interface, obviously. Qt's own QObject
s will have methods returning signal and slot objects, but those objects will not own the QObject
and will not attempt to delete it. Some examples:
// connect native signal to native slot
widget.signals().destroyed().connect(app.slots().quit());
// connect native signal to custom signal
let my_signal1 = qt_core::custom_signal::SignalI64::new();
file.signals().bytes_written().connect(&my_signal1);
The signal's connect()
function will return Result<meta_object::Connection>
, and the connection object will have disconnect()
method that is forwarded to this C++ method:
bool QObject::disconnect(const QMetaObject::Connection & connection)
Signal objects will also have disconnect()
method that receives the same data as connect()
and allows to disconnect signals without having a connection object.
Qt also has wildcard disconnect options that can be exposed. QObject
wrappers will have disconnect_all()
and disconnect_one()
methods to disconnect all receivers or one receiver (all signals and slots), and a signal object will have disconnect_all()
method to disconnect everything from this signal.
This is a work in progress. I will be happy to discuss the details and hear any suggestions.
Regarding this code
impl MyStruct {
pub fn new() -> MyStruct {
MyStruct {
my_data: 42,
my_signal1: qt_core::custom_signal::SignalI64::new(),
my_slot1: qt_core::custom_slot::SlotI64::new(|x| {
>> println!("received number: {}", x);
})
}
}
fn emit_example(&self) {
self.my_signal1.emit(42);
}
}
I can see that borrowing may be a problem here but if you can't send in self here there will be very little you actually can do inside this code if you don't have access to self so I think that needs to be solved some how.
You're right. To borrow self
, you'd need to wrap self
into a RefCell
and delay binding a closure a bit so that the variable is available. Something like this (not tested):
impl MyStruct {
pub fn new() -> Rc<RefCell<MyStruct>> {
let obj = Rc::new(RefCell::new(MyStruct {
my_data: 42,
my_signal1: qt_core::custom_signal::SignalI64::new(),
my_slot1: qt_core::custom_slot::SlotI64::new(),
}));
let obj_clone = obj.clone();
obj_clone.borrow_mut().my_slot1.bind(|x| {
println!("received number: {}, self: {:?}", x, obj.borrow());
});
obj_clone
}
}
Yeah. Not very pretty :/ Maybe it can be solved in another way but I don't have good suggestion right now.
I've done some thinking about this. Slots are so convenient in C++ because they just share this
pointer with other slots and all methods of the class. Actually, you can do the same in Rust: just make a pointer to self
and capture it in the lambdas. The only drawback is that you'll need unsafe dereference operation inside the lambdas. It's common practice in C++, but it's discouraged in Rust.
To make it work without unsafe, you'll need to put some effort in designing the code. Rc<RefCell<T>>
is quite common solution in such cases, and it's not all that ugly. I don't think there can be any prettier solution. It's the simplest implementation of an object with shared access that can't be statically checked.
However, it's even worse than it looks. When you call any function that could trigger a signal, there is a possibility that a Rust slot will be called by that function recursively. If this happens while some variable is borrowed from RefCell
and the slot attempts to borrow the same object, it will result in a panic. In C++, this simply results in recursive call and doesn't usually cause problems. Avoiding such panics in Rust can be hard. You'll need to either release the RefCell
borrow before calling the method, or block signals to prevent recursive calls, or introduce more granular RefCell
s to allow different fields to be borrowed simultaneously, or use queued connections everywhere. It's hard to keep track of this issue in a complicated application.
So writing safe (in Rust terms) code that works with slots is really complicated. Someone can just use raw pointer sharing. It's not all that bad. After all, using Qt already makes the program unsafe. Even if the generated API is declared as safe, it's easy to cause a segfault in Qt.
Yeah I have been thinking about this as well.
I think we have do go with unsafe here but but it would be nice if we could hide the unsafe parts under an API so the user don't see it.
Well, I guess we can hide it and provide a way to pass an arbitrary argument to the closure unsafely:
x.bind(&obj, |self1, arg| self1.some_method(arg));
But I'm not sure if it's good to hide things like that. If it can cause some unexpected issues, it's better to do it explicitly. We'll see after some experiments with real implementation.
Hi,
I have been fiddling around with my UI library that has some wrapping code for Qt and I have managed to get this running
struct MyTool {
button: PushButton,
some_value: i32,
}
impl MyTool {
fn new() -> Box<MyTool> {
Box::new(MyTool {
button: PushButton::new(),
some_value: 42,
})
}
fn button_callback(&mut self) {
self.some_value += 1;
println!("button callback, {}", self.some_value);
}
}
fn main() {
let app = Application::new();
let mut tool = MyTool::new();
connect_no_args!(&tool.button, push_button::RELEASED, &mut *tool, MyTool, MyTool::button_callback);
app.run();
}
Lots of this isn't really safe yet (will be hard to get it 100% safe anyway) and not very good looking but still I think it will be possible to get something nice running here.
You can find the code over here https://github.com/emoon/wrui_rust/blob/master/src/main.rs#L27 which uses this library https://github.com/emoon/wrui/blob/master/src/qt/wrui_qt.cpp
Notice that this code is very experimental and mostly a test-bed for me right now when hacking around.
Your version is closer to C++ API. I think users will definitely want something like that, just because it's convenient and customary, even if it's not as idiomatic by Rust's standards.
I want the base implementation of signals and slots to be as universal as possible. Ability to bind arbitrary closures will provide a lot of flexibility, and simpler cases like binding a struct's method can be easily implemented on top of the base implementation (assuming unsafe
is allowed).
I personally don't like macros very much because they hide what's really happening. I think the base implementation will not use macros at all. But I should provide optional macros that will simplify the code and make it more like your example. I may have to rethink my API a bit, though.
Yeah I'm not happy with the need for macro here but in this case it's really just a bridge for the callback down to the C(++) code and I'm still experimenting what would be the best approach.
@Riateche Have you thought any more about the signals/slot API design for Rust. I'm thinking about it again (would like to get away from the macro(s) but still allow same functionality) but I'm not really sure how I will be able to do it :/
I'm currently implementing slot wrappers as I've described earlier. I also want to get rid of AsBox
/AsStruct
markers in favor of smart defaults. After this is done, I will try to port some Qt examples and see if it gives me any ideas about API design.
Great! Looking forward to that.
Signal and slot wrappers are now implemented. The usability is not ideal, but at least all necessary pieces for interacting with built-in Qt signals and slots are now available.
I wonder if you have thought about how this would work? I was pointed to this on the users forum
https://github.com/White-Oak/qml-rust/blob/master/examples/qobjects.rs#L16
https://github.com/White-Oak/qml-rust/blob/master/src/macros.rs#L90
It seems fairly complex but I'm not sure how QObject works in details. Regardless slots & signals are a pretty big part of working with Qt so I wonder if you have any ideas of how this could be implemented?