The grpc module provides gRPC client/server and protobuf support for Qore, including:
- Dynamic protobuf schema loading from
.protofiles (no code generation needed) - Binary protobuf encoding/decoding via
ProtobufSchema - gRPC client with all four call patterns (unary, server streaming, client streaming, bidirectional)
- gRPC server with handler registration and async I/O
- TLS/SSL support on client and server
- Custom metadata passing (request, initial response, trailing)
- Timeout/deadline enforcement
- Connection pooling via
HttpClientConnectionManager - Built-in gRPC server reflection (
GrpcReflectionService) for client service discovery
The module combines three components:
- Binary module (
grpc): C++ bindings for Arrow schemas, record batches, and IPC usinglibarrow. - Core module (
protobuf): Dynamic schema loading and message encoding/decoding usingProtobufSchema. - Qore module (
GrpcUtil): Pure Qore implementing the gRPC protocol natively on Qore's HTTP/2 stack.
Three data provider modules are built on top of it:
| Module | What |
|---|---|
GrpcDataProvider |
any gRPC service as a data provider, with reflection-based discovery |
ArrowFlightDataProvider |
Apache Arrow Flight columnar data exchange |
SalesforcePubSubDataProvider |
Salesforce Pub/Sub API event source (gRPC + Avro) |
SalesforcePubSubDataProvider attaches itself to the Salesforce data provider in the Qore library
as its pubsub child, so an existing sfrest connection gains platform event, Change Data Capture
and custom channel event sources with no new connection type. See
design/salesforce-pubsub.md.
- Qore with the built-in protobuf module and HTTP/2 support (RPM builds use Qore 3.0+)
- CMake 3.21+
- C++17 compiler
- Apache Arrow development headers and shared libraries (
libarrow-develon Fedora/RHEL; the package must provideArrowConfig.cmake)
mkdir build && cd build
cmake ..
make
make install#!/usr/bin/env qore
%modern
%requires grpc
%requires GrpcUtil
ProtobufSchema schema("./proto/", "service.proto");
GrpcChannel channel("http://localhost:50051");
GrpcClient client(channel, schema, "MyService");
hash<GrpcCallResult> result = client.call("SayHello", {"name": "World"});
printf("Response: %y\n", result.body);
printf("Status: %d %s\n", result.status_code, result.status_message);
channel.shutdown();
ProtobufSchema schema("./proto/", "service.proto");
GrpcServer server(schema);
server.registerHandler("MyService", "SayHello",
hash<auto> sub(hash<auto> request, hash<string, string> metadata) {
return {"message": "Hello, " + request.name, "status": 0};
});
int port = server.addInsecurePort("localhost:0");
server.start();
server.wait();
# Client
GrpcClientStream stream = client.serverStream("ListItems", {"category": "books"});
while (*hash<auto> item = stream.read(5s)) {
printf("Item: %y\n", item);
}
hash<GrpcCallResult> result = stream.finish();
# Server handler
server.registerHandler("MyService", "ListItems",
sub(hash<auto> request, GrpcServerStream stream, hash<string, string> metadata) {
for (int i = 0; i < 10; ++i) {
stream.write({"name": sprintf("item-%d", i)});
}
});
# Client
GrpcClientStream stream = client.clientStream("Upload");
stream.write({"chunk": "data1"});
stream.write({"chunk": "data2"});
hash<GrpcCallResult> result = stream.finish();
# Server handler
server.registerHandler("MyService", "Upload",
hash<auto> sub(GrpcServerStream stream, hash<string, string> metadata) {
int count = 0;
while (*hash<auto> msg = stream.read()) {
++count;
}
return {"total": count};
});
The server handler processes messages incrementally -- no pre-buffering of the client stream. Both sides can interleave reads and writes for interactive communication:
# Client - interactive request/response
GrpcClientStream stream = client.bidiStream("Chat");
stream.write({"text": "Hello"});
*hash<auto> reply = stream.read(5s); # response arrives before next send
printf("Reply: %s\n", reply.text);
stream.write({"text": "World"});
reply = stream.read(5s);
printf("Reply: %s\n", reply.text);
stream.writesDone();
hash<GrpcCallResult> result = stream.finish();
# Server handler - echo each message immediately
server.registerHandler("MyService", "Chat",
sub(GrpcServerStream stream, hash<string, string> metadata) {
while (*hash<auto> msg = stream.read()) {
stream.write({"text": "Echo: " + msg.text});
}
});
# Server with TLS
string cert = ReadOnlyFile::readTextFile("server.crt");
string key = ReadOnlyFile::readTextFile("server.key");
int port = server.addSecurePort("localhost:0", <GrpcSslOptions>{
"server_cert": cert,
"server_key": key,
});
# Client with HTTPS
GrpcChannel channel("https://localhost:50051");
# Send metadata and set timeout
hash<GrpcCallOptions> opts = <GrpcCallOptions>{
"timeout_ms": 5000,
"metadata": {"x-request-id": "abc-123"},
};
hash<GrpcCallResult> result = client.call("Method", request, opts);
# Server receives metadata in handler
server.registerHandler("Service", "Method",
hash<auto> sub(hash<auto> request, hash<string, string> metadata) {
string req_id = metadata."x-request-id";
return {"id": req_id};
});
# Streaming handlers can set trailing metadata
server.registerHandler("Service", "Stream",
sub(hash<auto> request, GrpcServerStream stream, hash<string, string> metadata) {
stream.setTrailingMetadata({"x-count": "5"});
# ... write messages ...
});
Enable clients to discover services without local .proto files using the standard
gRPC server reflection protocol (grpc.reflection.v1). This works with tools like
grpcurl and grpc_cli as well as programmatic clients.
Just pass enable_reflection: True in the server options:
%modern
%requires grpc
%requires GrpcUtil
ProtobufSchema schema("./proto/", "service.proto");
GrpcServer server(schema, <GrpcServerOptions>{"enable_reflection": True});
server.registerHandler("MyService", "MyMethod",
hash<auto> sub(hash<auto> request, hash<string, string> metadata) {
return {"result": "ok"};
});
server.addInsecurePort("localhost:50051");
server.start();
server.wait();
# List all services exposed by the server
grpcurl -plaintext localhost:50051 list
# Describe a specific service
grpcurl -plaintext localhost:50051 describe mypackage.MyService
# Make a call without needing the .proto file
grpcurl -plaintext -d '{"name": "World"}' localhost:50051 mypackage.MyService/MyMethod%modern
%requires grpc
%requires GrpcUtil
GrpcChannel channel("http://localhost:50051");
# Discover services and schema via reflection
GrpcReflectionClient rc(channel);
list<string> services = rc.listServices();
printf("Available services: %y\n", services);
# Build a schema from the server's descriptors and make calls
ProtobufSchema discovered = rc.discoverSchema();
GrpcClient client(channel, discovered, "MyService");
hash<GrpcCallResult> result = client.call("MyMethod", {"name": "World"});
printf("Response: %y\n", result.body);
channel.shutdown();
The GrpcDataProvider module can use reflection for zero-configuration service access:
%modern
%requires GrpcDataProvider
GrpcDataProvider provider({
"url": "http://localhost:50051",
"use_reflection": True,
});
# Services are discovered automatically
auto result = provider.getChildProvider("MyService")
.getChildProvider("MyMethod")
.doRequest({"name": "World"});
Or from the command line:
qdp 'grpc{url=http://localhost:50051,use_reflection=true}/MyService/MyMethod' dor name=World%requires grpc
ProtobufSchema schema("./proto/", "messages.proto");
binary data = schema.encode("MyMessage", {"field": "value"});
hash<auto> msg = schema.decode("MyMessage", data);
# JSON conversion
string json = schema.toJson("MyMessage", {"field": "value"});
hash<auto> parsed = schema.fromJson("MyMessage", json);
The GrpcDataProvider module integrates gRPC with Qore's data provider framework, enabling
qdp CLI access and Qorus workflow integration.
# Using a proto file
qdp 'grpc{url=http://localhost:50051,proto_path=./proto,proto_file=service.proto}/MyService/SayHello' dor name=World
# Using server reflection (no proto files needed)
qdp 'grpc{url=http://localhost:50051,use_reflection=true}/MyService/SayHello' dor name=WorldServer-streaming methods expose an events child provider for event-driven consumption:
qdp 'grpc{url=http://localhost:50051,use_reflection=true}/PriceService/WatchPrices/events' listenBidirectional-streaming methods also expose an events child that supports both
receiving events and sending messages, enabling interactive use with qdp ix:
qdp 'grpc{url=http://localhost:50051,use_reflection=true}/ChatService/Chat/events' ixProgrammatic usage:
%modern
%requires GrpcDataProvider
GrpcDataProvider provider({
"url": "http://localhost:50051",
"use_reflection": True,
});
# Navigate to the events child of a bidi method
AbstractDataProvider events = provider.getChildProvider("ChatService")
.getChildProvider("Chat")
.getChildProvider("events");
# Register observer and start the stream
events.registerObserver(my_observer);
events.observersReady();
# Send messages
events.sendMessage(MESSAGE_GRPC_STREAM_SEND, {"text": "hello"});
events.sendMessage(MESSAGE_GRPC_STREAM_SEND, {"text": "world"});
# Signal writes done (server will finish and close the stream)
events.stopEvents();
MIT License - see LICENSE for details.
Copyright 2026 Qore Technologies, s.r.o.
With the Qore development package and its API indexes installed, build all five references and check their links and tables:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DQORE_GRPC_STRICT_DOCS=ON
cmake --build build --target docs
python3 -B -W error test/test-doc-index.py --build-dir build -vStrict mode rejects unresolved references. The output checks cover native and Qore APIs, links between modules, installed dependency indexes, protobuf table columns, and rejection of a deliberately invalid reference.