Qt 6.11 では、Qt GRPC ライブラリに安定性・安全性・パフォーマンスの向上と新機能が追加され、Qt で gRPC™ ベースのアプリケーションを構築する際の開発体験がさらに充実しました。
安定性・安全性・パフォーマンスの向上
今回のリリースにおける改善の多くは、内部的なものです。Qt GRPC の内部テスト基盤を大幅に強化し、プロトコルのコーナーケースやトランスポートレベルの動作など、これまでカバーが難しかった幅広いエッジケースを検証できるようになりました。このテストカバレッジの拡大により、多数の潜在的な問題が発見・修正されています。これらの修正の多くは Qt 6.8 を含む旧バージョンにバックポートされているため、長期サポート版をお使いのユーザーにもメリットがあります。
トランスポート面では、QGrpcHttp2Channel が圧縮メッセージの読み取りに対応しました。gRPC はプロトコルレベルでメッセージペイロードの圧縮をサポートしており、Qt GRPC クライアントはこの機能を利用するサーバーをシームレスに扱えるようになりました。
API の追加
今回のリリースには、小規模ながら実用的な API が 2 つ追加されています。
serverInitialMetadataReceived:サーバーの初期メタデータが到着した時点で発行するシグナルです。レスポンスメッセージを受け取る前に、検証・UI フィードバック・キャンセル判断などを早期に行えます。
メタデータフィルタリング: QGrpcChannelOptions と QGrpcCallOptions の両方で利用できるオプションで、プロトコル関連のサーバーメタデータを結果に含めるかどうかを制御します。デフォルトでは無効になっています。
インターセプター
Qt GRPC 6.11 では、クライアントサイドのインターセプター機能が導入されました。インターセプターは他の gRPC 実装でも広く使われているパターンであり、Qt GRPC でネイティブに利用できるようになりました。
インターセプターとは
インターセプターは、アプリケーションの呼び出し箇所を変更することなく、複数の RPC に横断的な処理を一貫して適用するための仕組みです。ロギング、認証、メトリクス収集、ポリシーの適用といった横断的関心事に適しています。
インターセプターは QGrpcInterceptorChain を通じてチャンネルに組み込まれ、RPC ライフサイクルの特定のフェーズにフックします。詳しくは Qt GRPC Interceptors Overview を参照してください。
チャットサンプルへのインターセプター追加
ここでは、Qt GRPC Chat サンプルにシンプルな LoggingInterceptor を追加する例を紹介します。このインターセプターはすべてのフックポイントでアクティビティをキャプチャし、構造化されたログエントリを LogModel に転送します。このモデルは QML に公開され、ListView で表示されます。
LoggingInterceptor は利用可能なすべてのインターセプションフックを再実装しており、起動からメッセージ交換、完了に至るまで RPC の各フェーズを監視・記録します。クラスの宣言を以下に示します。
class LoggingInterceptor final : public QGrpcStartInterceptor,
public QGrpcInitialMetadataInterceptor,
public QGrpcMessageReceivedInterceptor,
public QGrpcWriteMessageInterceptor,
public QGrpcWritesDoneInterceptor,
public QGrpcTrailingMetadataInterceptor,
public QGrpcCancelInterceptor,
public QGrpcFinishedInterceptor
{
public:
LoggingInterceptor(std::shared_ptr<LogModel> logModel);
~LoggingInterceptor() override;
Continuation onStart(QGrpcInterceptionContext &context, QProtobufMessage &message,
QGrpcCallOptions &callOptions) override;
void onInitialMetadata(QGrpcInterceptionContext &context,
QMultiHash<QByteArray, QByteArray> &metadata) override;
void onMessageReceived(QGrpcInterceptionContext &context, QByteArray &messageData) override;
void onWriteMessage(QGrpcInterceptionContext &context, QProtobufMessage &message) override;
void onWritesDone(QGrpcInterceptionContext &context) override;
void onTrailingMetadata(QGrpcInterceptionContext &context,
QMultiHash<QByteArray, QByteArray> &metadata) override;
void onCancel(QGrpcInterceptionContext &context) override;
void onFinished(QGrpcInterceptionContext &context, QGrpcStatus &status) override;
private:
std::shared_ptr<LogModel> mLog;
using Clock = std::chrono::steady_clock;
using Ms = std::chrono::duration<double, std::milli>;
QHash<quint64, Clock::time_point> m_activeRPCs;
};
実装はシンプルで、各インターセプションポイントで最も重要なイベントをログに記録しつつ、RPC 全体の所要時間を計測します。
#include "logginginterceptor.h"
#include <QtGrpc/QGrpcStatus>
using namespace Qt::Literals::StringLiterals;
LoggingInterceptor::LoggingInterceptor(std::shared_ptr<LogModel> logModel)
: mLog(std::move(logModel)) {
Q_ASSERT(mLog);
}
LoggingInterceptor::~LoggingInterceptor() = default;
LoggingInterceptor::Continuation LoggingInterceptor::onStart(QGrpcInterceptionContext &context,
QProtobufMessage &, QGrpcCallOptions &) {
const auto id = context.operationId();
m_activeRPCs.insert(id, Clock::now());
mLog->add(LogModel::Level::Debug, id, context.descriptor(), u"Starting"_s);
return Continuation::Proceed;
}
void LoggingInterceptor::onInitialMetadata(QGrpcInterceptionContext &context,
QMultiHash<QByteArray, QByteArray> &metadata) {
if (metadata.isEmpty())
return;
mLog->add(LogModel::Level::Info, context.operationId(), context.descriptor(),
u"Initial metadata: %1"_s.arg(QDebug::toString(metadata)));
}
void LoggingInterceptor::onMessageReceived(QGrpcInterceptionContext &context,
QByteArray &messageData) {
mLog->add(LogModel::Level::Debug, context.operationId(), context.descriptor(),
u"Received %2 bytes"_s.arg(messageData.size()));
}
void LoggingInterceptor::onWriteMessage(QGrpcInterceptionContext &context, QProtobufMessage &) {
mLog->add(LogModel::Level::Debug, context.operationId(), context.descriptor(),
u"About to write message"_s);
}
void LoggingInterceptor::onWritesDone(QGrpcInterceptionContext &context) {
mLog->add(LogModel::Level::Info, context.operationId(), context.descriptor(),
u"About to finish communication"_s);
}
void LoggingInterceptor::onTrailingMetadata(QGrpcInterceptionContext &context,
QMultiHash<QByteArray, QByteArray> &metadata) {
if (metadata.isEmpty())
return;
mLog->add(LogModel::Level::Info, context.operationId(), context.descriptor(),
u"Trailing metadata: %1"_s.arg(QDebug::toString(metadata)));
}
void LoggingInterceptor::onCancel(QGrpcInterceptionContext &context) {
mLog->add(LogModel::Level::Info, context.operationId(), context.descriptor(),
u"About to cancel the RPC"_s);
}
void LoggingInterceptor::onFinished(QGrpcInterceptionContext &context, QGrpcStatus &status) {
const auto it = m_activeRPCs.find(context.operationId());
Q_ASSERT(it != m_activeRPCs.cend());
const auto duration = Ms(Clock::now() - it.value()).count();
m_activeRPCs.erase(it);
const auto codeStr = QDebug::toString(status.code()).section("::", -1);
auto msg = u"Finished in %1 ms. StatusCode: %2"_s.arg(duration).arg(codeStr);
if (!status.message().isEmpty())
msg += u", Message: "_s + status.message();
const auto level = [&] {
switch (status.code()) {
case QtGrpc::StatusCode::Ok:
return LogModel::Level::Info;
case QtGrpc::StatusCode::NotFound:
case QtGrpc::StatusCode::Unauthenticated:
return LogModel::Level::Warning;
default:
return LogModel::Level::Error;
}
}();
mLog->add(level, context.operationId(), context.descriptor(), msg);
}
このインターセプターを組み込んだ状態で Qt GRPC Chat クライアントを実行すると、すべての RPC の詳細なログが表示されます。インターセプター 1 つで、チャンネル上で実行されるすべての呼び出しのライフサイクルを完全に把握できます。

執筆時点では、Qt GRPC Chat サンプルへのこれらの変更はまだマージされていませんが、次の Qt 6.11 パッチリリースに含まれる予定です。より高度な例として、Qt GRPC Client Guide サンプルでは、独立した 3 つのインターセプターが連携する複雑なワークフローを確認できます。
ご要望の機能や今後取り上げてほしいテーマがあれば、ぜひお知らせください。