asio-grpc
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.
Features
- 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 - 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
Example
- 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 and sharing a thread with an io_context can be found in the example directory.
Requirements
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.
Usage
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)
Available features
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.
CMake Options
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.
Performance
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, Boost 1.78, gRPC 1.45.0, asio-grpc v1.5.0, jemalloc 5.2.1
Results
1 CPU server
name | req/s | avg. latency | 90 % in | 95 % in | 99 % in | avg. cpu | avg. memory |
---|---|---|---|---|---|---|---|
rust_tonic_mt | 47057 | 21.08 ms | 9.45 ms | 10.30 ms | 538.94 ms | 101.99% | 23.03 MiB |
rust_thruster_mt | 42084 | 23.61 ms | 10.40 ms | 11.27 ms | 619.37 ms | 99.59% | 18.96 MiB |
cpp_asio_grpc_unifex | 41392 | 24.01 ms | 25.49 ms | 25.97 ms | 27.21 ms | 101.87% | 23.19 MiB |
rust_grpcio | 41036 | 24.19 ms | 25.77 ms | 26.45 ms | 27.56 ms | 102.04% | 37.3 MiB |
cpp_grpc_mt | 39971 | 24.88 ms | 26.37 ms | 26.79 ms | 28.24 ms | 101.49% | 17.95 MiB |
cpp_asio_grpc_callback | 39669 | 25.08 ms | 26.67 ms | 27.22 ms | 29.26 ms | 101.27% | 23.44 MiB |
cpp_asio_grpc_cpp20_coroutine | 34918 | 28.52 ms | 31.02 ms | 31.55 ms | 32.89 ms | 100.9% | 19.61 MiB |
cpp_grpc_callback | 12061 | 78.32 ms | 104.41 ms | 113.88 ms | 165.21 ms | 100.16% | 123.0 MiB |
go_grpc | 7391 | 128.34 ms | 220.03 ms | 299.23 ms | 432.17 ms | 98.03% | 29.62 MiB |
2 CPU server
name | req/s | avg. latency | 90 % in | 95 % in | 99 % in | avg. cpu | avg. memory |
---|---|---|---|---|---|---|---|
cpp_grpc_mt | 85780 | 10.04 ms | 18.31 ms | 22.16 ms | 30.52 ms | 200.78% | 48.37 MiB |
cpp_asio_grpc_unifex | 84826 | 10.07 ms | 18.46 ms | 22.92 ms | 33.97 ms | 200.05% | 43.57 MiB |
cpp_asio_grpc_callback | 83421 | 10.40 ms | 19.34 ms | 23.41 ms | 34.31 ms | 202.11% | 47.62 MiB |
cpp_asio_grpc_cpp20_coroutine | 76205 | 11.77 ms | 22.43 ms | 26.47 ms | 37.30 ms | 202.9% | 46.58 MiB |
rust_tonic_mt | 75512 | 12.42 ms | 33.21 ms | 51.89 ms | 79.70 ms | 201.96% | 19.17 MiB |
cpp_grpc_callback | 73730 | 11.99 ms | 21.13 ms | 27.39 ms | 41.39 ms | 206.76% | 153.53 MiB |
rust_thruster_mt | 67854 | 13.91 ms | 37.24 ms | 59.66 ms | 85.51 ms | 201.0% | 14.41 MiB |
rust_grpcio | 67496 | 14.22 ms | 21.79 ms | 23.92 ms | 27.76 ms | 200.15% | 37.24 MiB |
go_grpc | 16291 | 53.61 ms | 99.19 ms | 112.99 ms | 175.49 ms | 151.25% | 29.14 MiB |
Documentation
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
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)};
Where to go from here?
Check out the examples and the API documentation.
What users are saying
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.