Qt-Wrapper um das gRPC-Framework in C ++

Hallo an alle. Heute werden wir uns ansehen, wie Sie das gRPC-Framework in C ++ und die Qt-Bibliothek verknüpfen können. Der Artikel enthält Code, der die Verwendung aller vier Interaktionsmodi in gRPC zusammenfasst. Zusätzlich wird ein Code bereitgestellt, der die Verwendung von gRPC über Qt-Signale und -Slots ermöglicht. Der Artikel kann vor allem für Qt-Entwickler von Interesse sein, die an der Verwendung von gRPC interessiert sind. Trotzdem wird eine Verallgemeinerung der vier Betriebsmodi von gRPC in C ++ ohne Verwendung von Qt geschrieben, wodurch Entwickler, die nicht mit Qt verwandt sind, den Code anpassen können. Ich frage alle Interessierten unter Katze.


Hintergrund


Vor ungefähr sechs Monaten hingen zwei Projekte an mir, die die Client- und Serverteile von gRPC verwendeten. Beide Projekte gingen in Produktion. Diese Projekte wurden von Entwicklern geschrieben, die bereits gekündigt haben. Die einzige gute Nachricht war, dass ich mich aktiv am Schreiben des gRPC-Server- und Client-Codes beteiligt habe. Aber das war vor ungefähr einem Jahr. Deshalb musste ich mich wie immer von Grund auf um alles kümmern.


Der gRPC-Servercode wurde mit der Erwartung geschrieben, dass er von der .proto-Datei weiter generiert wird. Der Code wurde gut geschrieben. Der Server hatte jedoch einen großen Nachteil: Nur ein Client konnte eine Verbindung herstellen.


Der gRPC-Client wurde einfach schrecklich geschrieben.


Nur wenige Tage später fand ich den Client- und Servercode gRPC heraus. Und mir wurde klar, dass ich mich erneut mit dem Server und dem gRPC-Client befassen müsste, wenn ich ein Projekt für ein paar Wochen aufnehmen würde.


Damals entschied ich, dass es Zeit war, den gRPC-Client und -Server zu schreiben und zu debuggen, damit:


  • Sie konnten nachts ruhig schlafen;

  • Sie mussten sich nicht jedes Mal daran erinnern, wie dies funktioniert, wenn Sie einen Client oder einen gRPC-Server schreiben müssen.

  • Sie können den geschriebenen gRPC-Client und -Server in anderen Projekten verwenden.


Beim Schreiben von Code habe ich mich an folgenden Anforderungen orientiert:


  • Sowohl der gRPC-Client als auch der Server können auf natürliche Weise mit den Signalen und Slots der Qt-Bibliothek arbeiten.

  • Der gRPC-Client- und Servercode muss beim Ändern der .proto-Datei nicht repariert werden.

  • Der gRPC-Client sollte dem Clientcode den Status der Verbindung zum Server mitteilen können.


Die Struktur des Artikels ist wie folgt. Zunächst wird ein kurzer Überblick über die Ergebnisse der Arbeit mit Client-Code und eine kleine Erklärung dazu gegeben. Am Ende der Überprüfung ein Link zum Repository. Weiterhin wird es allgemeine Dinge zur Architektur geben. Dann eine Beschreibung des Server- und Client-Codes (was sich unter der Haube befindet) und eine Schlussfolgerung.


Kurzer Rückblick


Die einfachste pingproto.proto-Datei wurde als .proto-Datei verwendet, in der RPCs aller Arten von Interaktionen definiert wurden:


syntax = "proto3"; package pingpong; service ping { rpc SayHello (PingRequest) returns (PingReply) {} rpc GladToSeeMe(PingRequest) returns (stream PingReply){} rpc GladToSeeYou(stream PingRequest) returns (PingReply){} rpc BothGladToSee(stream PingRequest) returns (stream PingReply){} } message PingRequest { string name = 1; string message = 2; } message PingReply { string message = 1; } 

Die Datei pingpong.proto wiederholt die Datei helloworld.proto aus dem Artikel über asynchrone gRPC-Modi in C ++ bis zum genauen Namen.


Infolgedessen kann ein geschriebener Server wie folgt verwendet werden:


 class A: public QObject { Q_OBJECT; QpingServerService pingservice; public: A() { bool is_ok; is_ok = connect(&pingservice, SIGNAL(SayHelloRequest(SayHelloCallData*)), this, SLOT(onSayHello(SayHelloCallData*))); assert(is_ok); is_ok = connect(&pingservice, SIGNAL(GladToSeeMeRequest(GladToSeeMeCallData*)), this, SLOT(onGladToSeeMe(GladToSeeMeCallData*))); assert(is_ok); is_ok = connect(&pingservice, SIGNAL(GladToSeeYouRequest(GladToSeeYouCallData*)), this, SLOT(onGladToSeeYou(GladToSeeYouCallData*))); assert(is_ok); is_ok = connect(&pingservice, SIGNAL(BothGladToSeeRequest(BothGladToSeeCallData*)), this, SLOT(onBothGladToSee(BothGladToSeeCallData*))); assert(is_ok); } public slots: void onSayHello(SayHelloCallData* cd) { std::cout << "[" << cd->peer() << "][11]: request: " << cd->request.name() << std::endl; cd->reply.set_message("hello " + cd->request.name()); cd->Finish(); } //etc. }; 

Wenn ein Client RPC aufruft, benachrichtigt der gRPC-Server den Clientcode (in diesem Fall Klasse A) mit dem entsprechenden Signal.


Der gRPC-Client kann folgendermaßen verwendet werden:


 class B : public QObject { Q_OBJECT QpingClientService pingPongSrv; public: B() { bool c = false; c = connect(&pingPongSrv, SIGNAL(SayHelloResponse(SayHelloCallData*)), this, SLOT(onSayHelloResponse(SayHelloCallData*))); assert(c); c = connect(&pingPongSrv, SIGNAL(GladToSeeMeResponse(GladToSeeMeCallData*)), this, SLOT(onGladToSeeMeResponse(GladToSeeMeCallData*))); assert(c); c = connect(&pingPongSrv, SIGNAL(GladToSeeYouResponse(GladToSeeYouCallData*)), this, SLOT(onGladToSeeYouResponse(GladToSeeYouCallData*))); assert(c); c = connect(&pingPongSrv, SIGNAL(BothGladToSeeResponse(BothGladToSeeCallData*)), this, SLOT(onBothGladToSeeResponse(BothGladToSeeCallData*))); assert(c); c = connect(&pingPongSrv, SIGNAL(channelStateChanged(int, int)), this, SLOT(onPingPongStateChanged(int, int))); assert(c); } void usage() { //Unary PingRequest request; request.set_name("user"); request.set_message("user"); pingPongSrv.SayHello(request); //Server streaming PingRequest request2; request2.set_name("user"); pingPongSrv.GladToSeeMe(request2); //etc. } public slots: void SayHelloResponse(SayHelloCallData* response) { std::cout << "[11]: reply: " << response->reply.message() << std::endl; if (response->CouldBeDeleted()) delete response; } //etc. }; 

Mit dem gRPC-Client können Sie RPC direkt aufrufen und die Antwort des Servers mit den entsprechenden Signalen abonnieren.


Der gRPC-Client hat auch ein Signal:

 channelStateChanged(int, int); 

Hiermit werden vergangene und aktuelle Serververbindungsstatus gemeldet. Der gesamte Beispielcode befindet sich im qgrpc-Repository .

Wie funktioniert es?


Das Prinzip der Einbeziehung des Clients und des gRPC-Servers in das Projekt ist in der Abbildung dargestellt.



In der .pro-Projektdatei werden .proto-Dateien angegeben, auf deren Grundlage gRPC funktioniert. Die Datei grpc.pri enthält Befehle zum Generieren von gRPC- und QgRPC-Dateien. Der Protoc-Compiler generiert gRPC-Dateien [protofile] .grpc.pb.h und [protofile] .grpc.pb.cc. [protofile] ist der Name der .proto-Datei, die an die Compiler-Eingabe übergeben wird.


Die Generierung von QgRPC-Dateien [protofile] .qgrpc. [Config] .h wird vom Skript genQGrpc.py übernommen. [config] ist entweder "Server" oder "Client".

Die generierten QgRPC-Dateien enthalten einen Qt-Wrapper um gRPC-Klassen und Aufrufe mit entsprechenden Signalen. In den vorherigen Beispielen wurden die Klassen QpingServerService und QpingClientService in den generierten Dateien pingpong.qgrpc.server.h und pingpong.qgrpc.client.h deklariert. Generierte QgRPC-Dateien werden der MOC-Verarbeitung hinzugefügt.


In den generierten QgRPC-Dateien sind die QGrpc [config] .h-Dateien enthalten, in denen alle Hauptarbeiten stattfinden. Lesen Sie weiter unten mehr darüber.


Um all diese Konstruktionen mit dem Projekt zu verbinden, müssen Sie die Datei grpc.pri in die Projekt-Pro-Datei aufnehmen und drei Variablen angeben. Die GRPC-Variable definiert .proto-Dateien, die an die Eingaben des Protoc-Compilers und des Skripts genQGrpc.py übertragen werden. Die Variable QGRPC_CONFIG definiert den Konfigurationswert der generierten QgRPC-Dateien und kann die Werte "Server" oder "Client" enthalten. Sie können auch die optionale Variable GRPC_VERSION definieren, um die Version von gRPC anzugeben.


Weitere Informationen zu allem Gesagten finden Sie in den Beispieldateien grpc.pri und .pro.


Serverarchitektur


Das Serverklassendiagramm ist in der Abbildung dargestellt.



Die dicken Pfeile zeigen die Hierarchie der Klassenvererbung und die dünnen Pfeile zeigen die Zugehörigkeit von Mitgliedern und Methoden zu Klassen. Im Allgemeinen wird die ServerService-Klasse Q [Dienstname] für den Dienst generiert, wobei Dienstname der Name des in der .proto-Datei deklarierten Dienstes ist. RPCCallData sind Kontrollstrukturen, die für jeden RPC im Service generiert werden. Im Konstruktor der QpingServerService-Klasse wird die Basisklasse QGrpcServerService mit dem asynchronen Dienst gRPC pingpong :: ping :: AsyncService initialisiert. Um den Dienst zu starten, müssen Sie die Start () -Methode mit der Adresse und dem Port aufrufen, auf dem der Dienst ausgeführt wird. Die Funktion Start () implementiert die Standardprozedur zum Starten eines Dienstes.


Am Ende der Funktion Start () wird die reine virtuelle Funktion makeRequests () aufgerufen, die in der generierten Klasse QpingServerService implementiert ist:


 void makeRequests() { needAnotherCallData< SayHello_RPCtypes, SayHelloCallData >(); needAnotherCallData< GladToSeeMe_RPCtypes, GladToSeeMeCallData >(); needAnotherCallData< GladToSeeYou_RPCtypes, GladToSeeYouCallData >(); needAnotherCallData< BothGladToSee_RPCtypes, BothGladToSeeCallData >(); } 

Der zweite Vorlagenparameter der Funktion needAnotherCallData sind die generierten RPCCallData-Strukturen. Die gleichen Strukturen sind die Parameter der Signale in der erzeugten Qt-Klasse des Dienstes.


Die generierten RPCCallData-Strukturen erben von der ServerCallData-Klasse. Die ServerCallData-Klasse wird wiederum vom ServerResponder-Responder geerbt. Die Schaffung eines Objekts mit kohärenten Strukturen führt somit zur Schaffung eines Antwortobjekts.


Der Konstruktor für die ServerCallData-Klasse akzeptiert zwei Parameter: signal_func und request_func. signal_func ist ein generiertes Signal, das nach dem Empfang eines Tags aus der Warteschlange aufgerufen wird. request_func ist eine Funktion, die beim Erstellen eines neuen Responders aufgerufen werden sollte. In diesem Fall kann es sich beispielsweise um die Funktion RequestSayHello () handeln. Der Aufruf request_func erfolgt in der Funktion needAnotherCallData (). Dies erfolgt so, dass die Verwaltung der Befragten (Erstellung und Löschung) im Dienst erfolgt.


Der Code der Funktion needAnotherCallData () besteht aus dem Erstellen eines Responderobjekts und dem Aufrufen einer Funktion, die den Responder mit einem RPC-Aufruf verbindet:


 template<class RPCCallData, class RPCTypes> void needAnotherCallData() { RPCCallData* cd = new RPCCallData(); //... RequestRPC<RPCTypes::kind, ...> (service_, cd->request_func_, cd->responder, ..., (void*)cd); } 

Die RequestRPC () -Funktionen sind Vorlagenfunktionen für vier Arten von Interaktionen. Infolgedessen führt der Aufruf von RequestRPC () zu einem Aufruf:


 service_->(cd->request_func_)(...,cd->responder, (void*)cd); 

Dabei ist service_ der gRPC-Dienst. In diesem Fall handelt es sich um pingpong :: ping :: AsyncService.


Um die Ereigniswarteschlange synchron oder asynchron zu überprüfen, müssen Sie die Funktionen CheckCQ () bzw. AsyncCheckCQ () aufrufen. Der Code der CheckCQ () - Funktion besteht aus Aufrufen des synchronen Tags aus der Warteschlange und der Verarbeitung dieses Tags:


 virtual void CheckCQ() override { void* tag; bool ok; server_cq_->Next(&tag, &ok); //tagActions_ call if (!tag) return; AbstractCallData* cd = (AbstractCallData*)tag; if (!started_.load()) { destroyCallData(cd); return; } cd->cqReaction(this, ok); } 

Nach dem Empfang des Tags aus der Warteschlange werden die Gültigkeit des Tags und der Serverstart überprüft. Wenn der Server ausgeschaltet ist, wird das Tag nicht mehr benötigt - es kann gelöscht werden. Danach heißt die in der ServerCallData-Klasse definierte Funktion cqReaction ():


 void cqReaction(const QGrpcServerService* service_, bool ok) { if (!first_time_reaction_) { first_time_reaction_ = true; service_->needAnotherCallData<RPC, RPCCallData>(); } auto genRpcCallData = dynamic_cast<RPCCallData*>(this); void* tag = static_cast<void*>(genRpcCallData); if (this->CouldBeDeleted()) { service_->destroyCallData(this); return; } if (!this->processEvent(tag, ok)) return; //call generated service signal with generated call data argument service_->(*signal_func_)(genRpcCallData); } 

Das Flag first_time_reaction_ gibt an, dass Sie einen neuen Responder für den aufgerufenen RPC erstellen müssen. Die Funktionen CouldBeDeleted () und ProcessEvent () werden von der entsprechenden ServerResponder-Responderklasse geerbt. Die Funktion CouldBeDeleted () gibt ein Zeichen zurück, dass das Responder-Objekt gelöscht werden kann. Die Funktion processEvent () verarbeitet das Tag- und das OK-Flag. Für einen Responder vom Typ Client-Streaming sieht die Funktion beispielsweise folgendermaßen aus:


 bool processEvent(void* tag, bool ok) { this->tag_ = tag; read_mode_ = ok; return true; } 

Die ProcessEvent () -Funktion gibt unabhängig vom Typ des Responders immer true zurück. Der Rückgabewert dieser Funktion bleibt für eine mögliche Erweiterung der Funktionalität und theoretisch zur Beseitigung von Fehlern übrig.


Nach der Verarbeitung des Ereignisses folgt der Aufruf:

 service_->(*signal_func_)(genRpcCallData); 

Die Variable service_ ist eine Instanz des generierten Dienstes, in unserem Fall QpingServerService. Die Variable signal_func_ ist ein Dienstsignal, das einem bestimmten RPC entspricht. Zum Beispiel SayHelloRequest (). Die Variable genRpcCallData ist ein Responderobjekt des entsprechenden Typs. Aus Sicht des aufrufenden Codes ist die Variable genRpcCallData ein Objekt einer der generierten RPCCallData-Strukturen.


Kundenarchitektur


Wann immer möglich, stimmen die Namen der Klassen und Funktionen des Clients mit den Namen der Klassen und Funktionen des Servers überein. Das Clientklassendiagramm ist in der Abbildung dargestellt.



Die dicken Pfeile zeigen die Hierarchie der Klassenvererbung und die dünnen Pfeile zeigen die Zugehörigkeit von Mitgliedern und Methoden zu Klassen. Im Allgemeinen wird für den Dienst die ClientService-Klasse Q [Dienstname] generiert, wobei Dienstname der Name des in der .proto-Datei deklarierten Dienstes ist. RPCCallData sind Kontrollstrukturen, die für jeden RPC im Service generiert werden. Um einen RPC aufzurufen, stellt die generierte Klasse Funktionen bereit, deren Namen genau mit dem in der .proto-Datei deklarierten RPC übereinstimmen. In unserem Beispiel wird SayHello () in der .proto-RPC-Datei wie folgt deklariert:

 rpc SayHello (PingRequest) returns (PingReply) {} 

In der generierten QpingClientService-Klasse sieht die entsprechende RPC-Funktion folgendermaßen aus:


 void SayHello(PingRequest request) { if(!connected()) return; SayHelloCallData* call = new SayHelloCallData; call->request = request; call->responder = stub_->AsyncSayHello(&call->context, request, &cq_); call->responder->Finish(&call->reply, &call->status, (void*)call); } 

Die generierten RPCCallData-Strukturen werden wie im Fall des Servers letztendlich von der ClientResponder-Klasse geerbt. Daher führt die Erstellung eines Objekts der generierten Struktur zur Erstellung eines Responders. Nach dem Erstellen des Responders wird der RPC aufgerufen und der Responder dem Ereignis zugeordnet, bei dem eine Antwort vom Server empfangen wird. In Bezug auf den Clientcode sieht ein RPC-Aufruf folgendermaßen aus:


 void ToSayHello() { PingRequest request; request.set_name("user"); request.set_message("user"); pingPongSrv.SayHello(request); } 

Im Gegensatz zur generierten QpingServerService-Serverklasse erbt die QpingClientService-Klasse von zwei Vorlagenklassen: ConnectivityFeatures und MonitorFeatures.


Die ConnectivityFeatures-Klasse ist für den Status der Client-Server-Verbindung verantwortlich und bietet drei zu verwendende Funktionen: grpc_connect (), grpc_disconnect (), grpc_reconnect (). Die Funktion grpc_disconnect () entfernt einfach alle Datenstrukturen, die für die Interaktion mit dem Server verantwortlich sind. Der Aufruf von grpc_connect reduziert sich auf Aufrufe der Funktion grpc_connect_ (), mit der Steuerdatenstrukturen erstellt werden:


 void grpc_connect_() { channel_ = grpc::CreateChannel(target_, creds_); stub_ = GRPCService::NewStub(channel_); channelFeatures_ = std::make_unique<ChannelFeatures>(channel_); channelFeatures_->checkChannelState(); } 

Die ChannelFeatures-Klasse überwacht den Status der channel_ channel-Kommunikation mit dem Server. Die ConnectivityFeatures-Klasse kapselt ein Objekt der ChannelFeatures-Klasse und implementiert die abstrakten Funktionen channelState (), checkChannelState () und connection () unter Verwendung dieses Objekts. Die Funktion channelState () gibt den zuletzt beobachteten Status des Kommunikationskanals mit dem Server zurück. Die Funktion checkChannelState () gibt tatsächlich den aktuellen Status des Kanals zurück. Die Funktion linked () gibt das Vorzeichen des Clients zurück, der eine Verbindung zum Server herstellt.


Die MonitorFeatures-Klasse ist für den Empfang und die Verarbeitung von Ereignissen vom Server verantwortlich und stellt die CheckCQ () -Funktion zur Verwendung bereit:


 bool CheckCQ() { auto service_ = dynamic_cast< SERVICE* >(this); //connection state auto old_state = conn_->channelState(); auto new_state = conn_->checkChannelState(); if (old_state != new_state) service->*channelStateChangedSignal_(old_state, new_state); //end of connection state void* tag; bool ok = false; grpc::CompletionQueue::NextStatus st; st = cq_.AsyncNext(&tag, &ok, deadlineFromMSec(100)); if ((st == grpc::CompletionQueue::SHUTDOWN) || (st == grpc::CompletionQueue::TIMEOUT)) return false; (AbstractCallData< SERVICE >*)(tag)->cqActions(service_, ok); return true; } 

Die Codestruktur ist dieselbe wie im Fall des Servers. Im Gegensatz zum Server wird dem Client ein Codeblock hinzugefügt, der für die Verarbeitung des aktuellen Status verantwortlich ist. Wenn sich der Status des Kommunikationskanals geändert hat, wird das Signal channelStateChangedSignal_ () aufgerufen. Bei allen generierten Diensten ist dies ein Signal:

 void channelStateChanged(int, int); 

Im Gegensatz zum Server wird hier anstelle von Next () auch die Funktion AsyncNext () verwendet. Dies wurde aus mehreren Gründen getan. Erstens kann der Clientcode bei Verwendung von AsyncNext () Informationen über die Änderung des Status des Kommunikationskanals erhalten. Zweitens ist es bei Verwendung von AsyncNext () möglich, verschiedene RPCs im Client-Code beliebig oft aufzurufen. Wenn Sie in diesem Fall die Funktion Next () verwenden, wird der Thread blockiert, bis ein Ereignis aus der Warteschlange empfangen wird, und dadurch werden die beiden beschriebenen Funktionen verloren.

Nach dem Empfang des Ereignisses aus der Warteschlange wird wie im Fall des Servers die in der ClientCallData-Klasse definierte Funktion cqReaction () aufgerufen:


 void cqActions(RPC::Service* service, bool ok) { auto response = dynamic_cast<RPCCallData*>(this); void* tag = static_cast<void*>(response); if (!this->processEvent(tag, ok)) return; service->*func_( response ); } 

Wie beim Server verarbeitet die Funktion processEvent () das Tag- und das OK-Flag und gibt immer true zurück. Wie im Fall des Servers sollte nach der Verarbeitung des Ereignisses das Signal des generierten Dienstes aufgerufen werden. Es gibt jedoch zwei signifikante Unterschiede zur gleichnamigen Serverfunktion. Der erste Unterschied besteht darin, dass in dieser Funktion keine Responder erstellt werden. Die Erstellung von Respondern erfolgt wie oben gezeigt, wenn der RPC aufgerufen wird. Der zweite Unterschied besteht darin, dass Responder in dieser Funktion nicht gelöscht werden. Das Fehlen der Entfernung von Respondern erfolgt aus zwei Gründen. Erstens kann Client-Code Zeiger verwenden, um RPCCallData-Strukturen für ihre eigenen Zwecke zu generieren. Das Löschen von Inhalten mit diesem Zeiger, die im Clientcode verborgen sind, kann zu unangenehmen Konsequenzen führen. Zweitens führt das Entfernen des Responders dazu, dass kein Signal mit Daten erzeugt wird. Daher empfängt der Clientcode nicht die letzte Servernachricht. Unter mehreren Alternativen zur Lösung der angegebenen Probleme wurde die Entscheidung getroffen, das Entfernen des Responders (generierte Strukturen) auf den Client-Code zu verschieben. Daher müssen die Signalhandlerfunktionen (Slots) den folgenden Code enthalten:


 void ResponseHandler(RPCCallData* response) { if (response->CouldBeDeleted()) delete response; //process response } 

Das Fehlen des Entfernens des Responders im Client-Code führt nicht nur zu einem Speicherverlust, sondern auch zu möglichen Problemen mit dem Kommunikationskanal. Signalhandler aller Arten von RPC-Interaktionen sind im Beispielcode implementiert.


Fazit


Abschließend machen wir auf zwei Punkte aufmerksam. Der erste Punkt bezieht sich auf den Aufruf der CheckCQ () - Funktionen des Clients und des Servers. Sie funktionieren wie oben gezeigt nach einem Prinzip: Wenn sich ein Ereignis in der Warteschlange befindet, wird ein Signal mit der entsprechenden generierten RPCCallData-Struktur "emittiert". Sie können diese Funktion manuell aufrufen und (im Fall eines Clients) nach einem Ereignis suchen. Zunächst bestand jedoch die Idee, den gesamten mit gRPC verknüpften Netzwerkteil auf einen anderen Thread zu übertragen. Zu diesem Zweck wurden die Hilfsklassen QGrpcSrvMonitor für den gRPC-Server und QGrpcCliServer für den gRPC-Client geschrieben. Beide Klassen arbeiten nach demselben Prinzip: Sie erstellen einen separaten Stream, fügen den generierten Service in diesen Stream ein und rufen regelmäßig die CheckCQ () - Funktion dieses Service auf. Wenn Sie beide Hilfsklassen verwenden, müssen Sie daher keine CheckCQ () -Funktionen im Clientcode aufrufen. Die Signale des generierten Dienstes "kommen" in diesem Fall von einem anderen Stream. Client- und Serverbeispiele werden mithilfe dieser Hilfsklassen implementiert.


Der zweite Punkt betrifft die Mehrheit der Entwickler, die die Qt-Bibliothek in ihrer Arbeit nicht verwenden. Qt-Klassen und Makros in QgRPC werden nur an zwei Stellen verwendet: in generierten Servicedateien und in Dateien, die Zusatzklassen enthalten: QGrpcServerMonitor.h und QGrpcClientMonitor.h. Die verbleibenden Dateien mit der Qt-Bibliothek sind in keiner Weise zugeordnet. Es war geplant, Assembly mit cmake hinzuzufügen und einige Qt-Anweisungen zu stubben. Insbesondere die QObject-Klasse und das Q_OBJECT-Makro. Aber die Hände kamen einfach nicht dazu. Vorschläge sind daher willkommen.


Das ist alles. Danke an alle!


Referenzen


Source: https://habr.com/ru/post/de420237/


All Articles