feat: Add Qt bindings - #76
Conversation
|
I converted to draft because I want to first settle the corresponding usage in deltatouch, see https://codeberg.org/lk108/deltatouch/pulls/269 and chatmail/core#8330 |
That facet package/repo allows LLM usage (https://github.com/facet-rs/facet/tree/main/.claude) which may or may not be a bad sign for a core dependency. There is a feature request for supporting 1password's typeshare crate: #58, I think that may be interesting to explore for generating types for other languages. |
Move Method to module Add qt types generation Generate qt methods
link2xt
left a comment
There was a problem hiding this comment.
Have not looked at yerpc/src/qt.rs and yerpc/src/type_info.rs yet.
Generation of bindings are not really tested, but adding C++ tests in CI is likely not easy, so as long as DeltaTouch is using it we can probably merge it somewhat quickly if https://codeberg.org/lk108/deltatouch/pulls/269 is already using it (other than replacing all calls) and there are no problems discovered.
| )); | ||
| gen_methods_qt.push(quote!( | ||
| let args = vec![#(#gen_args),*]; | ||
| let method = Method::new(#ts_name, #rpc_name, args, #gen_output, #is_notification, #is_positional, #docs); |
There was a problem hiding this comment.
ts_name should probably be renamed to something like camel_name since it is used for Qt as well now. As far as i understand it is accidental that Qt and TypeScript both use camel case.
There was a problem hiding this comment.
Good point! I also updated the field name in Method accordingly 546f1f9
|
|
||
| template<typename T> | ||
| struct [[nodiscard]] Result { | ||
| T result; |
There was a problem hiding this comment.
A comment saying that this is the default value of T in case of error would be nice here as valueOrDefault implementation seems to depend on it.
There was a problem hiding this comment.
You mean sth like: /// Value contained in the result. In the case of an error this is a default constructed T
Thinking about this a bit more: For the docs in valueOrDefault to be true ("If the result is an error [...] returns a default-constructed T.") I should probably just return T() in valueOrDefault. Let me know if you'd like it.
| QJsonObject err = val["error"].toObject(); | ||
| if (err.isEmpty()) | ||
| return {{}, "Invalid error in response: " + QJsonDocument(val).toJson(QJsonDocument::Compact), -32700}; | ||
| return {{}, err["message"].toString(), err["code"].toInt()}; |
There was a problem hiding this comment.
If there is no code (should normally not happen since there is an error message), this returns QJsonValue::Null (according to https://doc.qt.io/qt-6/qjsonobject.html#operator-5b-5d) and will evaluate to 0.
Maybe makes sense to convert it to int above and if it is 0, return custom error (also to fail if error code is 0, so we don't accidentally treat the default value as the real return value in this case).
There was a problem hiding this comment.
Good point, thanks! I changed the check to !error_message.isString() || error_code == 0 b67cde9
| class Transport { | ||
| public: | ||
| virtual std::future<Result<QJsonValue>> send(const QString method, const QJsonValue request) = 0; | ||
| // virtual void send_notify(const QJsonValue request) = 0; not implemented |
There was a problem hiding this comment.
Can probably be simply removed? If someone decides later to implement yerpc support for notifications, then they will add a way to clients somehow.
| if (val.contains("error")) { | ||
| QJsonObject err = val["error"].toObject(); | ||
| if (err.isEmpty()) | ||
| return {{}, "Invalid error in response: " + QJsonDocument(val).toJson(QJsonDocument::Compact), -32700}; |
There was a problem hiding this comment.
Just a comment: this is apparently still the standard way for C++ to initialize this kind of structures. "Designated initializers" ({.result = ..., error_message = ...) are only supported since C++20 (and DeltaTouch currently uses C++11 and maybe C++17 with the PR switching to these bindings).
Otherwise there is even std::expected to map results directly, but only since C++23. Don't know how good is the support for it currently and if DeltaTouch can switch to it.
There was a problem hiding this comment.
Yes DeltaTouch would be using C++17. Because we still want to support Ubuntu Touch 20.04 we don't have a recent enough GCC version for newer C++ versions than C++17.
| @@ -0,0 +1,326 @@ | |||
| use typescript_type_def::type_expr as ts; | |||
There was a problem hiding this comment.
note: Somewhat unexpected that this module that is generic depends on something having "typescript" in it, but as far as i understand this is only because the types in JSON are essentially JavaScript types and this has nothing to do with typescript bindings here.
link2xt
left a comment
There was a problem hiding this comment.
Looks good, i think can be merged already so it can be used in DeltaTouch.
| } | ||
| .to_owned(), | ||
| TypeInfo::Optional(o) => { | ||
| format!("std::optional<{}>", o.qt_type()) |
There was a problem hiding this comment.
A note for other reviewers, from someone not familiar with Qt: it looks like there is no QOptional or anything similar, so std::optional type is expected here:
https://stackoverflow.com/questions/33736172/does-qt-have-its-own-boostoptional-alternative
| String, | ||
| Optional(Box<TypeInfo>), | ||
| Array(Box<TypeInfo>), | ||
| Map(Box<TypeInfo>), |
There was a problem hiding this comment.
Not obvious from the type: the keys are always strings. A documentation comment saying that the type is the type of values could be helpful here.
| writeln!( | ||
| output, | ||
| r#" | ||
| inline QJsonValue toJson(const {ty} &o) {{ return static_cast<double>(o); }} |
There was a problem hiding this comment.
Another note: QJsonValue has no types for integers other than double: https://doc.qt.io/qt-6/qjsonvalue.html#Type-enum
This is also the reason for int53 types in Telegram APIs, 53-bit IDs are safe to store in double, but if they get larger then they may not be represented exactly.
This is a bit dangerous if used to represent something that may actually get larger than 53-bit, e.g. maybe file sizes etc.
This adds qt binding generation. It uses the json parsing from qt. A transport-implementation is needed to use it, similar to typescript. I have an implementation for deltachat-cffi but i think it would go in the chatmail-core repo.
Click to expand the CffiTransport implementation
I created a hopefully generally useful
TypeInfotype, which can be created fromTypeDef::SHAPE. This should allow easier future expansions for other (C-like) languages.Future improvements: Add docs for generated types. Currently only the rpc methods themself are documentd.
Sidenote: I discarded an ealier draft to create bindings which tried to first implement json parsing on the C-layer with a swap-able json-parser implementation (to support both cjson and qtjson). Then C++ wrappers were added ontop of the C-Layer. But writing safe C code and interop is hard and the generated code was quite involved and hard to understand, all in all it got quite complicated. This approach is much simpler, it just works for qt, but the generated code is straight forward.
Sidenote 2: I also took a look into https://facet.rs/ as a replacement for
typescript-type-defsderive(TypeDef). It is an extensible reflection framework and an alternative to serde. https://docs.rs/facet-typescript could in theory replace our typescript generation. I did not investigate further because we'd have to either have to switch to use facet also for json de-/serialization or we'd have to duplicate all the#serde(..)annotations in deltachat-jsonrpc. (Facet is probably slower at runtime than serde.) The nice thing abouttypescript-type-defis that it reuses the serde annotations.Another related advencement is reflection and comptime in rust which might make the derives superfluous all together one day.