An Executor, Networking TS and std::execution interface to grpc::CompletionQueue for writing asynchronous gRPC clients and servers using C++20 coroutines, Boost.Coroutines, Asio's stackless coroutines, callbacks, sender/receiver and more.
- Asio ExecutionContext compatible wrapper around grpc::CompletionQueue
- Executor and Networking TS requirements fulfilling associated executor
- Support for all RPC types: unary, client-streaming, server-streaming and bidirectional-streaming with any mix of Asio CompletionToken as well as TypedSender, including allocator customization
- Support for asynchronously waiting for grpc::Alarms including cancellation through cancellation_slots and StopTokens
- Initial support for
std::execution
concepts through libunifex and Asio: schedule, connect, submit, scheduler, typed_sender and more - Support for generic gRPC clients and servers (aka. proxies)
- Experimental support for Golang/Rust
select
-style programming with the help of cancellation safety - No-Boost version with standalone Asio
- No-Asio version with libunifex
- CMake function to generate gRPC source files: asio_grpc_protobuf_generate
- Server side 'hello world':
std::unique_ptr<grpc::Server> server;
grpc::ServerBuilder builder;
agrpc::GrpcContext grpc_context{builder.AddCompletionQueue()};
builder.AddListeningPort(host, grpc::InsecureServerCredentials());
helloworld::Greeter::AsyncService service;
builder.RegisterService(&service);
server = builder.BuildAndStart();
boost::asio::co_spawn(
grpc_context,
[&]() -> boost::asio::awaitable<void>
{
grpc::ServerContext server_context;
helloworld::HelloRequest request;
grpc::ServerAsyncResponseWriter<helloworld::HelloReply> writer{&server_context};
co_await agrpc::request(&helloworld::Greeter::AsyncService::RequestSayHello, service, server_context,
request, writer);
helloworld::HelloReply response;
response.set_message("Hello " + request.name());
co_await agrpc::finish(writer, response, grpc::Status::OK);
},
boost::asio::detached);
grpc_context.run();
More examples for things like streaming RPCs, double-buffered file transfer with io_uring, libunifex-based coroutines, sharing a thread with an io_context and generic clients/servers can be found in the example directory.
If you got any wishes or ideas for new features to asio-grpc then do not hesitate to open an issue. Some food for thought: A high-level API (what would that look like?), rate-limiting and protobuf::Arena
support in agrpc::repeatedly_request
, .... I am also happy to help with general gRPC and Asio questions.
Tested by CI:
- gRPC 1.44.0, 1.16.1 (older versions might work as well)
- Boost 1.79.0 (min. 1.74.0)
- Standalone Asio 1.17.0 (min. 1.17.0)
- libunifex 2022-02-09
- MSVC 19.31 (Visual Studio 17 2022)
- GCC 8.4.0, 9.3.0, 10.3.0, 11.1.0
- Clang 10.0.0, 11.0.0, 12.0.0
- AppleClang 13.0.0.13000029
- C++17 and C++20
For MSVC compilers and asio-grpc before v1.6.0 the following compile definitions need to be set:
BOOST_ASIO_HAS_DEDUCED_REQUIRE_MEMBER_TRAIT
BOOST_ASIO_HAS_DEDUCED_EXECUTE_MEMBER_TRAIT
BOOST_ASIO_HAS_DEDUCED_EQUALITY_COMPARABLE_TRAIT
BOOST_ASIO_HAS_DEDUCED_QUERY_MEMBER_TRAIT
BOOST_ASIO_HAS_DEDUCED_QUERY_STATIC_CONSTEXPR_MEMBER_TRAIT
BOOST_ASIO_HAS_DEDUCED_PREFER_MEMBER_TRAIT
When using standalone Asio then omit the BOOST_
prefix.
The library can be added to a CMake project using either add_subdirectory
or find_package
. Once set up, include the individual headers from the agrpc/ directory or the combined header:
#include <agrpc/asioGrpc.hpp>
As a subdirectory
Clone the repository into a subdirectory of your CMake project. Then add it and link it to your target.
Using Boost.Asio:
find_package(gRPC)
find_package(Boost)
add_subdirectory(/path/to/repository/root)
target_link_libraries(your_app PUBLIC gRPC::grpc++ asio-grpc::asio-grpc Boost::headers)
Or using standalone Asio:
find_package(gRPC)
find_package(asio)
add_subdirectory(/path/to/repository/root)
target_link_libraries(your_app PUBLIC gRPC::grpc++ asio-grpc::asio-grpc-standalone-asio asio::asio)
Or using libunifex:
find_package(gRPC)
find_package(unifex)
add_subdirectory(/path/to/repository/root)
target_link_libraries(your_app PUBLIC gRPC::grpc++ asio-grpc::asio-grpc-unifex unifex::unifex)
As a CMake package
Clone the repository and install it.
cmake -B build -DCMAKE_INSTALL_PREFIX=/desired/installation/directory .
cmake --build build --target install
Locate it and link it to your target.
Using Boost.Asio:
# Make sure CMAKE_PREFIX_PATH contains /desired/installation/directory
find_package(asio-grpc)
target_link_libraries(your_app PUBLIC asio-grpc::asio-grpc)
Or using standalone Asio:
# Make sure CMAKE_PREFIX_PATH contains /desired/installation/directory
find_package(asio-grpc)
target_link_libraries(your_app PUBLIC asio-grpc::asio-grpc-standalone-asio)
Or using libunifex:
# Make sure CMAKE_PREFIX_PATH contains /desired/installation/directory
find_package(asio-grpc)
target_link_libraries(your_app PUBLIC asio-grpc::asio-grpc-unifex)
Using vcpkg
Add asio-grpc to the dependencies inside your vcpkg.json
:
{
"name": "your_app",
"version": "0.1.0",
"dependencies": [
"asio-grpc",
// To use the Boost.Asio backend add
// "boost-asio",
// To use the standalone Asio backend add
// "asio",
// To use the libunifex backend add
// "libunifex"
]
}
Locate asio-grpc and link it to your target in your CMakeLists.txt
:
find_package(asio-grpc)
# Using the Boost.Asio backend
target_link_libraries(your_app PUBLIC asio-grpc::asio-grpc)
# Or use the standalone Asio backend
#target_link_libraries(your_app PUBLIC asio-grpc::asio-grpc-standalone-asio)
# Or use the libunifex backend
#target_link_libraries(your_app PUBLIC asio-grpc::asio-grpc-unifex)
boost-container
- Use Boost.Container instead of <memory_resource>
See selecting-library-features to learn how to select features with vcpkg.
Using Hunter
See asio-grpc's documentation on the Hunter website: https://hunter.readthedocs.io/en/latest/packages/pkg/asio-grpc.html.
ASIO_GRPC_USE_BOOST_CONTAINER
- Use Boost.Container instead of <memory_resource>
.
ASIO_GRPC_DISABLE_AUTOLINK
- Set before using find_package(asio-grpc)
to prevent asio-grpcConfig.cmake
from finding and setting up interface link libraries.
asio-grpc is part of grpc_bench. Head over there to compare its performance against other libraries and languages.
Results from the helloworld unary RPC
Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz, Linux, GCC 11.3.0, Boost 1.79.0, gRPC 1.45.2, asio-grpc v1.6.0, jemalloc 5.2.1
Request scenario: string_100B
Results
name | req/s | avg. latency | 90 % in | 95 % in | 99 % in | avg. cpu | avg. memory |
---|---|---|---|---|---|---|---|
go_grpc | 48531 | 19.91 ms | 30.14 ms | 33.31 ms | 40.12 ms | 101.89% | 24.08 MiB |
rust_thruster_mt | 40478 | 24.55 ms | 10.96 ms | 12.74 ms | 627.82 ms | 104.12% | 11.25 MiB |
rust_tonic_mt | 40472 | 24.53 ms | 10.54 ms | 11.53 ms | 656.24 ms | 100.81% | 13.95 MiB |
cpp_grpc_mt | 38273 | 26.00 ms | 27.70 ms | 28.28 ms | 29.93 ms | 102.03% | 5.49 MiB |
rust_grpcio | 37160 | 26.80 ms | 28.57 ms | 29.12 ms | 30.16 ms | 103.11% | 16.99 MiB |
cpp_asio_grpc_unifex | 36932 | 26.95 ms | 28.71 ms | 29.16 ms | 30.63 ms | 102.42% | 5.76 MiB |
cpp_asio_grpc_callback | 36910 | 26.96 ms | 28.95 ms | 29.55 ms | 31.10 ms | 102.38% | 5.43 MiB |
cpp_asio_grpc_cpp20_coroutine | 32536 | 30.60 ms | 32.83 ms | 33.35 ms | 34.54 ms | 102.38% | 5.57 MiB |
cpp_asio_grpc_poll_context_coro | 32261 | 30.87 ms | 32.84 ms | 33.28 ms | 34.59 ms | 97.29% | 5.46 MiB |
cpp_grpc_callback | 10678 | 85.92 ms | 110.19 ms | 155.75 ms | 169.82 ms | 100.86% | 46.71 MiB |
name | req/s | avg. latency | 90 % in | 95 % in | 99 % in | avg. cpu | avg. memory |
---|---|---|---|---|---|---|---|
cpp_asio_grpc_unifex | 84129 | 9.93 ms | 15.18 ms | 18.19 ms | 26.90 ms | 197.04% | 29.37 MiB |
cpp_grpc_mt | 82149 | 10.13 ms | 15.80 ms | 19.14 ms | 27.80 ms | 197.31% | 30.58 MiB |
cpp_asio_grpc_callback | 81859 | 10.21 ms | 16.11 ms | 19.63 ms | 28.74 ms | 201.94% | 27.8 MiB |
cpp_asio_grpc_cpp20_coroutine | 76178 | 11.27 ms | 17.79 ms | 21.07 ms | 29.60 ms | 206.57% | 27.12 MiB |
cpp_grpc_callback | 71953 | 11.33 ms | 19.93 ms | 24.16 ms | 34.00 ms | 207.53% | 68.95 MiB |
cpp_asio_grpc_poll_context_coro | 69887 | 12.35 ms | 21.89 ms | 25.85 ms | 36.03 ms | 196.94% | 30.94 MiB |
go_grpc | 67081 | 13.03 ms | 20.37 ms | 23.56 ms | 30.64 ms | 196.18% | 25.92 MiB |
rust_tonic_mt | 59653 | 15.71 ms | 41.71 ms | 63.73 ms | 98.95 ms | 203.49% | 15.97 MiB |
rust_thruster_mt | 59262 | 15.62 ms | 44.36 ms | 78.45 ms | 100.23 ms | 203.42% | 12.91 MiB |
rust_grpcio | 58375 | 16.10 ms | 23.77 ms | 26.31 ms | 32.31 ms | 214.5% | 30.29 MiB |
The main workhorses of this library are the agrpc::GrpcContext
and its executor_type
- agrpc::GrpcExecutor
.
The agrpc::GrpcContext
implements asio::execution_context and can be used as an argument to Asio functions that expect an ExecutionContext
like asio::spawn.
Likewise, the agrpc::GrpcExecutor
satisfies the Executor and Networking TS and Scheduler requirements and can therefore be used in places where Asio/libunifex expects an Executor
or Scheduler
.
The API for RPCs is modeled closely after the asynchronous, tag-based API of gRPC. As an example, the equivalent for grpc::ClientAsyncReader<helloworld::HelloReply>.Read(helloworld::HelloReply*, void*)
would be agrpc::read(grpc::ClientAsyncReader<helloworld::HelloReply>&, helloworld::HelloReply&, CompletionToken)
.
Instead of the void*
tag in the gRPC API the functions in this library expect a CompletionToken. Asio comes with several CompletionTokens already: C++20 coroutine, stackless coroutine, callback and Boost.Coroutine. There is also a special token created by agrpc::use_sender(scheduler)
that causes RPC functions to return a TypedSender.
If you are interested in learning more about the implementation details of this library then check out this blog article.
Getting started
Start by creating a agrpc::GrpcContext
.
For servers and clients:
grpc::ServerBuilder builder;
agrpc::GrpcContext grpc_context{builder.AddCompletionQueue()};
For clients only:
agrpc::GrpcContext grpc_context{std::make_unique<grpc::CompletionQueue>()};
Add some work to the grpc_context
and run it. Make sure to shutdown the server
before destructing the grpc_context
. Also destruct the grpc_context
before destructing the server
. A grpc_context
can only be run on one thread at a time.
grpc_context.run();
server->Shutdown();
} // grpc_context is destructed here before the server
It might also be helpful to create a work guard before running the agrpc::GrpcContext
to prevent grpc_context.run()
from returning early.
std::optional guard{asio::require(grpc_context.get_executor(), asio::execution::outstanding_work_t::tracked)};
Check out the examples and the API documentation.
Asio-grpc abstracts away the implementation details of asynchronous grpc handling: crafting working code is easier, faster, less prone to errors and considerably more fun. At 3YOURMIND we reliably use asio-grpc in production since its very first release, allowing our developers to effortlessly implement low-latency/high-throughput asynchronous data transfer in time critical applications.
Our project is a real-time distributed motion capture system that uses your framework to stream data back and forward between multiple machines. Previously I have tried to build a bidirectional streaming framework from scratch using only gRPC. However, it's not maintainable and error-prone due to a large amount of service and streaming code. As a developer whose experienced both raw grpc and asio-grpc, I can tell that your framework is a real a game-changer for writing grpc code in C++. It has made my life much easier. I really appreciate the effort you have put into this project and your superior skills in designing c++ template code.