diff --git a/CMakeLists.txt b/CMakeLists.txt index 8f0f2e4501..acc585abdf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,45 +2,51 @@ cmake_minimum_required(VERSION 3.16) project(deltachat LANGUAGES C) include(GNUInstallDirs) +option(WITH_JSONRPC_BINDINGS "Generate jsonrpc bindings" OFF) + find_program(CARGO cargo) if(APPLE) - set(DYNAMIC_EXT "dylib") + set(DYNAMIC_EXT "dylib") elseif(UNIX) - set(DYNAMIC_EXT "so") + set(DYNAMIC_EXT "so") else() - set(DYNAMIC_EXT "dll") + set(DYNAMIC_EXT "dll") endif() if(DEFINED ENV{CARGO_BUILD_TARGET}) - set(ARCH_DIR "$ENV{CARGO_BUILD_TARGET}") + set(CARGO_OUT_DIR "${CMAKE_BINARY_DIR}/target/$ENV{CARGO_BUILD_TARGET}/release") else() - set(ARCH_DIR "./") + set(CARGO_OUT_DIR "${CMAKE_BINARY_DIR}/target/release") endif() -add_custom_command( - OUTPUT - "${CMAKE_BINARY_DIR}/target/release/libdeltachat.a" - "${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}" - "${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc" - COMMAND - PREFIX=${CMAKE_INSTALL_PREFIX} - LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR} - INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR} - ${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi -) +if(WITH_JSONRPC_BINDINGS) + set(JSONRPC_ARGS --package deltachat-jsonrpc-bindings) +endif() add_custom_target( - lib_deltachat - ALL - DEPENDS - "${CMAKE_BINARY_DIR}/target/release/libdeltachat.a" - "${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}" - "${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc" + lib_deltachat + ALL + COMMAND + ${CMAKE_COMMAND} -E env + CARGO_TARGET_DIR="${CMAKE_BINARY_DIR}/target" + PREFIX="${CMAKE_INSTALL_PREFIX}" + LIBDIR="${CMAKE_INSTALL_FULL_LIBDIR}" + INCLUDEDIR="${CMAKE_INSTALL_FULL_INCLUDEDIR}" + ${CARGO} build --release --package deltachat_ffi ${JSONRPC_ARGS} + WORKING_DIRECTORY + "${CMAKE_CURRENT_SOURCE_DIR}" ) -install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) -install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR}) -install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR}) -install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) +install(FILES "deltachat-ffi/deltachat.h" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") +install(FILES "${CARGO_OUT_DIR}/libdeltachat.a" DESTINATION "${CMAKE_INSTALL_LIBDIR}") +install(FILES "${CARGO_OUT_DIR}/libdeltachat.${DYNAMIC_EXT}" DESTINATION "${CMAKE_INSTALL_LIBDIR}") +install(FILES "${CARGO_OUT_DIR}/pkgconfig/deltachat.pc" DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig") + +if(WITH_JSONRPC_BINDINGS) + set(JSONRPC_HPP "${CMAKE_CURRENT_SOURCE_DIR}/deltachat-jsonrpc-bindings/qt/deltachat-jsonrpc") + install(FILES "${JSONRPC_HPP}/generated/types.hpp" "${JSONRPC_HPP}/generated/client.hpp" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/deltachat-jsonrpc/generated") + install(FILES "${JSONRPC_HPP}/cffi_client.hpp" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/deltachat-jsonrpc") +endif() diff --git a/Cargo.lock b/Cargo.lock index c8d5e8bca3..3cc870f339 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1444,6 +1444,13 @@ dependencies = [ "yerpc", ] +[[package]] +name = "deltachat-jsonrpc-bindings" +version = "2.51.0" +dependencies = [ + "deltachat-jsonrpc", +] + [[package]] name = "deltachat-repl" version = "2.51.0" @@ -7443,8 +7450,7 @@ dependencies = [ [[package]] name = "yerpc" version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dc24983fbe850227bfc1de89bf8cbfb3e2463afc322e0de2f155c4c23d06445" +source = "git+https://github.com/d2weber/yerpc.git?branch=qt_bindings#9e4cfb963b1c113a0c0701ea29865a9099b45949" dependencies = [ "anyhow", "async-channel 1.9.0", @@ -7462,9 +7468,8 @@ dependencies = [ [[package]] name = "yerpc_derive" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d8560d021437420316370db865e44c000bf86380b47cf05e49be9d652042bf5" +version = "0.6.4" +source = "git+https://github.com/d2weber/yerpc.git?branch=qt_bindings#9e4cfb963b1c113a0c0701ea29865a9099b45949" dependencies = [ "convert_case", "darling", diff --git a/Cargo.toml b/Cargo.toml index 978269b726..f06c5b597c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -130,6 +130,7 @@ members = [ "deltachat-ffi", "deltachat_derive", "deltachat-jsonrpc", + "deltachat-jsonrpc-bindings", "deltachat-rpc-server", "deltachat-ratelimit", "deltachat-repl", @@ -203,7 +204,7 @@ thiserror = "2" tokio = "1" tokio-util = "0.7.18" tracing-subscriber = "0.3" -yerpc = "0.6.4" +yerpc = { git="https://github.com/d2weber/yerpc.git", branch="qt_bindings" } [features] default = ["vendored"] diff --git a/deltachat-jsonrpc-bindings/.gitignore b/deltachat-jsonrpc-bindings/.gitignore new file mode 100644 index 0000000000..86d4c2dd38 --- /dev/null +++ b/deltachat-jsonrpc-bindings/.gitignore @@ -0,0 +1 @@ +generated diff --git a/deltachat-jsonrpc-bindings/Cargo.toml b/deltachat-jsonrpc-bindings/Cargo.toml new file mode 100644 index 0000000000..8c3ba22749 --- /dev/null +++ b/deltachat-jsonrpc-bindings/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "deltachat-jsonrpc-bindings" +version = "2.51.0" +edition = "2024" + +[build-dependencies] +deltachat-jsonrpc = { workspace = true } + +[features] +default = ["vendored"] +vendored = ["deltachat-jsonrpc/vendored"] diff --git a/deltachat-jsonrpc-bindings/build.rs b/deltachat-jsonrpc-bindings/build.rs new file mode 100644 index 0000000000..bedbd04b1f --- /dev/null +++ b/deltachat-jsonrpc-bindings/build.rs @@ -0,0 +1,6 @@ +use deltachat_jsonrpc::api::{generate_qt_bindings, generate_ts_bindings}; + +fn main() { + generate_ts_bindings(); + generate_qt_bindings("deltachat"); +} diff --git a/deltachat-jsonrpc-bindings/qt/deltachat-jsonrpc/cffi_client.hpp b/deltachat-jsonrpc-bindings/qt/deltachat-jsonrpc/cffi_client.hpp new file mode 100644 index 0000000000..be19f8d99e --- /dev/null +++ b/deltachat-jsonrpc-bindings/qt/deltachat-jsonrpc/cffi_client.hpp @@ -0,0 +1,104 @@ +#pragma once + +#include "deltachat-jsonrpc/generated/types.hpp" +#include "deltachat-jsonrpc/generated/client.hpp" +#include "deltachat.h" + +#include +#include +#include + +namespace deltachat { + +class CffiTransport : public Transport { +public: + explicit CffiTransport(dc_accounts_t* accounts) + : jsonrpc_(dc_jsonrpc_init(accounts)) + { + if (!jsonrpc_) std::abort(); + thread_ = std::thread([this] { run(); }); + } + + virtual ~CffiTransport() { + 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\"}"); + if (thread_.joinable()) thread_.join(); + std::lock_guard lk(mu_); + for (auto& [id, prom] : pending_) { + prom.set_value({{}, "Transport destructed", -32060}); + } + pending_.clear(); + if (jsonrpc_) dc_jsonrpc_unref(jsonrpc_); + } + + virtual std::future> send(const QString method, const QJsonValue params) override { + uint32_t id = next_id_++; + QJsonObject envelope{ + {"jsonrpc", "2.0"}, + {"id", static_cast(id)}, + {"method", method}, + {"params", params}, + }; + + std::promise> prom; + std::future> fut = prom.get_future(); + + { + std::lock_guard lk(mu_); + pending_[id] = std::move(prom); + } + + QByteArray json = QJsonDocument(envelope).toJson(QJsonDocument::Compact); + dc_jsonrpc_request(jsonrpc_, json.constData()); + return fut; + } +private: + void run() { + 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(obj["id"].toInt()); + + std::promise> prom; + { + std::lock_guard 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_; + std::thread thread_; + std::mutex mu_; + std::atomic next_id_{1}; + std::atomic done_{false}; + std::unordered_map>> pending_; +}; + +class CffiDeltaChat : public RawClient { +public: + explicit CffiDeltaChat(dc_accounts_t* accounts) + : RawClient(std::make_unique(accounts)) {} +}; + +} diff --git a/deltachat-jsonrpc-bindings/src/main.rs b/deltachat-jsonrpc-bindings/src/main.rs new file mode 100644 index 0000000000..e7a11a969c --- /dev/null +++ b/deltachat-jsonrpc-bindings/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index e70c2771b8..422afa9bf3 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -157,7 +157,11 @@ impl CommandApi { } } -#[rpc(all_positional, ts_outdir = "typescript/generated")] +#[rpc( + all_positional, + ts_outdir = "typescript/generated", + qt_outdir = "qt/deltachat-jsonrpc/generated" +)] impl CommandApi { /// Test function. async fn sleep(&self, delay: f64) { diff --git a/flake.nix b/flake.nix index 615a99c24a..c04821979d 100644 --- a/flake.nix +++ b/flake.nix @@ -51,6 +51,7 @@ ./deltachat-contact-tools ./deltachat-ffi ./deltachat-jsonrpc + ./deltachat-jsonrpc-bindings ./deltachat-ratelimit ./deltachat-repl ./deltachat-rpc-client