Skip to content

feat: Add Qt bindings - #76

Open
d2weber wants to merge 5 commits into
chatmail:mainfrom
d2weber:qt_bindings
Open

feat: Add Qt bindings#76
d2weber wants to merge 5 commits into
chatmail:mainfrom
d2weber:qt_bindings

Conversation

@d2weber

@d2weber d2weber commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

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
class CffiTransport : public QThread, public Transport {
    Q_OBJECT

public:
    explicit CffiTransport(dc_accounts_t* accounts, QObject* parent = nullptr)
        : QThread(parent)
        , jsonrpc_(dc_jsonrpc_init(accounts))
    {
        if (!jsonrpc_) std::abort();
        start();
    }

    virtual ~CffiTransport() override {
        done_ = true;
        // Unblock dc_jsonrpc_next_response by sending a dummy request
        if (jsonrpc_) dc_jsonrpc_request(jsonrpc_, "{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"get_system_info\"}");
        wait();
        QMutexLocker lk(&mu_);
        for (auto& [id, prom] : pending_) {
            prom.set_value({{}, "Transport destructed", -32060});
        }
        pending_.clear();
        if (jsonrpc_) dc_jsonrpc_unref(jsonrpc_);
    }

    virtual std::future<Result<QJsonValue>> send(const QString method, const QJsonValue params) override {
        uint32_t id = next_id_++;
        QJsonObject envelope{
            {"jsonrpc", "2.0"},
            {"id", static_cast<qint64>(id)},
            {"method", method},
            {"params", params},
        };

        std::promise<Result<QJsonValue>> prom;
        std::future<Result<QJsonValue>> fut = prom.get_future();

        {
            QMutexLocker lk(&mu_);
            pending_[id] = std::move(prom);
        }

        QByteArray json = QJsonDocument(envelope).toJson(QJsonDocument::Compact);
        dc_jsonrpc_request(jsonrpc_, json.constData());
        return fut;
    }
protected:
    void run() override {
        while (!done_) {
            char* raw_json = dc_jsonrpc_next_response(jsonrpc_);
            if (!raw_json) {
              break;
            }
            QByteArray json{raw_json};
            dc_str_unref(raw_json);
            if (done_) break;

            QJsonObject obj = QJsonDocument::fromJson(json).object();

            if (!obj["id"].isDouble()) {
              qCritical() << "No valid rpc id in" << QString{json};
              continue;
            }
            uint32_t id = static_cast<uint32_t>(obj["id"].toInt());

            std::promise<Result<QJsonValue>> prom;
            {
                QMutexLocker lk(&mu_);
                if (auto nh = pending_.extract(id)) {
                  prom = std::move(nh.mapped());
                } else {
                  qCritical() << "Could not map response" << QString{json};
                  continue;
                }
            }
            prom.set_value(parseResult(obj));
        }
    }

private:
    dc_jsonrpc_instance_t* jsonrpc_;
    QMutex mu_;
    std::atomic<uint32_t> next_id_{1};
    std::atomic<bool> done_{false};
    std::unordered_map<uint32_t, std::promise<Result<QJsonValue>>> pending_;
};

I created a hopefully generally useful TypeInfo type, which can be created from TypeDef::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-defs derive(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 about typescript-type-def is 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.

@d2weber
d2weber marked this pull request as draft June 13, 2026 07:48
@d2weber

d2weber commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator Author

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

@Simon-Laux

Simon-Laux commented Jun 26, 2026

Copy link
Copy Markdown
Member

Sidenote 2: I also took a look into https://facet.rs/ as a replacement

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.
Also, serde is basically the standard in the ecosystem, so I wouldn't want to switch to something that is less proven.

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.
The other idea was to use the OpenRPC spec to generate all bindings, but OpenRPC was not a mature/clear/good as I and others hoped. And generators for other languages were still in a very early or non-existent state at the time.
https://github.com/chatmail/dcrpcgen is our attempt at writing a code generator for python and go from the OpenRPC spec.

Move Method to module
Add qt types generation
Generate qt methods
@d2weber d2weber changed the title Add Qt bindings feat: Add Qt bindings Jul 16, 2026
@d2weber
d2weber marked this pull request as ready for review July 16, 2026 16:13
@d2weber
d2weber requested a review from link2xt August 19, 2026 18:02

@link2xt link2xt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also copy-pasted, strange double comment

struct [[nodiscard]] Result {
T result;
QString error_message;
int32_t error_code = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)) {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)

Comment thread yerpc/src/type_info.rs
@@ -0,0 +1,326 @@
use typescript_type_def::type_expr as ts;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants