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.
| // Write qt types to file. | ||
| export_types_to_file::<__AllTyps>(&outdir.join("types.hpp"), root_namespace).expect("Failed to write Qt out"); | ||
| // remove __AllTyps type from output, | ||
| // it's only used as a woraround to export all types and is not needed anymore now |
There was a problem hiding this comment.
| // it's only used as a woraround to export all types and is not needed anymore now | |
| // it's only used as a workaround to export all types and is not needed anymore now |
(it is copy-pasted from what is now ts_impl, but anyway)
| .write_all(new_content.as_bytes()) | ||
| .expect("removing __AllTyps from Qt failed"); | ||
|
|
||
| // // Generate a raw client. |
There was a problem hiding this comment.
Also copy-pasted, strange double comment
| struct [[nodiscard]] Result { | ||
| T result; | ||
| QString error_message; | ||
| int32_t error_code = 0; |
There was a problem hiding this comment.
This default value does not seem to be used (all fields are manually initialized everywhere anyway), so maybe not set it.
|
|
||
| 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.
| 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).
| 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.
| }); | ||
| } | ||
| public: | ||
| RawClient(std::unique_ptr<Transport> t) : transport_(std::move(t)) {} |
There was a problem hiding this comment.
| RawClient(std::unique_ptr<Transport> t) : transport_(std::move(t)) {} | |
| RawClient(std::unique_ptr<Transport> t) : transport_{std::move(t)} {} |
(does not really matter here, but i think this is the recommended way since C++11)
| @@ -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.
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.