blob: 6356dc34efcf0da671be52224d778b35c9931224 [file] [log] [blame]
.. _module-pw_rpc-guides:
===================
Quickstart & guides
===================
.. pigweed-module-subpage::
:name: pw_rpc
.. _module-pw_rpc-quickstart:
----------
Quickstart
----------
This section guides you through the process of adding ``pw_rpc`` to your project.
Check out :ref:`module-pw_rpc-design-overview` for a conceptual explanation
of the RPC lifecycle.
1. RPC service declaration
==========================
Declare a :ref:`service <module-pw_rpc-design-services>` in a service
definition:
.. code-block:: protobuf
/* //applications/blinky/blinky.proto */
syntax = "proto3";
package blinky;
import "pw_protobuf_protos/common.proto";
service Blinky {
// Toggles the LED on or off.
rpc ToggleLed(pw.protobuf.Empty) returns (pw.protobuf.Empty);
// Continuously blinks the board LED a specified number of times.
rpc Blink(BlinkRequest) returns (pw.protobuf.Empty);
}
message BlinkRequest {
// The interval at which to blink the LED, in milliseconds.
uint32 interval_ms = 1;
// The number of times to blink the LED.
optional uint32 blink_count = 2;
} // BlinkRequest
Declare the protocol buffer in ``BUILD.bazel``:
.. code-block:: python
# //applications/blinky/BUILD.bazel
load(
"@pigweed//pw_protobuf_compiler:pw_proto_library.bzl",
"nanopb_proto_library",
"nanopb_rpc_proto_library",
)
proto_library(
name = "proto",
srcs = ["blinky.proto"],
deps = [
"@pigweed//pw_protobuf:common_proto",
],
)
nanopb_proto_library(
name = "nanopb",
deps = [":proto"],
)
nanopb_rpc_proto_library(
name = "nanopb_rpc",
nanopb_proto_library_deps = [":nanopb"],
deps = [":proto"],
)
.. _module-pw_rpc-quickstart-codegen:
2. RPC code generation
======================
How do you interact with this protobuf from C++? ``pw_rpc`` uses
codegen libraries to automatically transform ``.proto`` definitions
into C++ header files. These headers are generated in the build
output directory. Their exact location varies by build system and toolchain
but the C++ include path always matches the source declarations in
``proto_library``. The ``.proto`` extension is replaced with an extension
corresponding to the protobuf codegen library in use:
.. csv-table::
:header: "Protobuf libraries", "Build subtarget", "Protobuf header", "pw_rpc header"
"Raw only", "``.raw_rpc``", "(none)", "``.raw_rpc.pb.h``"
"Nanopb or raw", "``.nanopb_rpc``", "``.pb.h``", "``.rpc.pb.h``"
"pw_protobuf or raw", "``.pwpb_rpc``", "``.pwpb.h``", "``.rpc.pwpb.h``"
Most projects should default to Nanopb. See
:ref:`module-pw_rpc-guides-headers`.
For example, the generated RPC header for ``applications/blinky.proto`` is
``applications/blinky.rpc.pb.h`` for Nanopb or
``applications/blinky.raw_rpc.pb.h`` for raw RPCs.
The generated header defines a base class for each RPC service declared in the
``.proto`` file. A service named ``TheService`` in package ``foo.bar`` would
generate the following base class for ``pw_protobuf``:
.. cpp:class:: template <typename Implementation> foo::bar::pw_rpc::pwpb::TheService::Service
3. RPC service definition
=========================
Implement a service class by inheriting from the generated RPC service
base class and defining a method for each RPC. The methods must match the name
and function signature for one of the supported protobuf implementations.
Services may mix and match protobuf implementations within one service.
A Nanopb implementation of the ``Blinky`` service looks like this:
.. code-block:: cpp
/* //applications/blinky/main.cc */
// ...
#include "applications/blinky/blinky.rpc.pb.h"
#include "pw_system/rpc_server.h"
// ...
class BlinkyService final
: public blinky::pw_rpc::nanopb::Blinky::Service<BlinkyService> {
public:
pw::Status ToggleLed(const pw_protobuf_Empty &, pw_protobuf_Empty &) {
// Turn the LED off if it's currently on and vice versa
}
pw::Status Blink(const blinky_BlinkRequest &request, pw_protobuf_Empty &) {
if (request.blink_count == 0) {
// Stop blinking
}
if (request.interval_ms > 0) {
// Change the blink interval
}
if (request.has_blink_count) { // Auto-generated property
// Blink request.blink_count times
}
}
};
BlinkyService blinky_service;
namespace pw::system {
void UserAppInit() {
// ...
pw::system::GetRpcServer().RegisterService(blinky_service);
}
} // namespace pw::system
Declare the implementation in ``BUILD.bazel``:
.. code-block:: python
# //applications/blinky/BUILD.bazel
cc_binary(
name = "blinky",
srcs = ["main.cc"],
deps = [
":nanopb_rpc",
# ...
"@pigweed//pw_system",
],
)
.. tip::
The generated code includes RPC service implementation stubs. You can
reference or copy and paste these to get started with implementing a service.
These stub classes are generated at the bottom of the pw_rpc proto header.
To use the stubs, do the following:
#. Locate the generated RPC header in the build directory. For example:
.. code-block:: sh
cd bazel-out && find . -name blinky.rpc.pb.h
#. Scroll to the bottom of the generated RPC header.
#. Copy the stub class declaration to a header file.
#. Copy the member function definitions to a source file.
#. Rename the class or change the namespace, if desired.
#. List these files in a build target with a dependency on
``proto_library``.
4. Register the service with a server
=====================================
Set up the server and register the service:
.. code-block:: cpp
/* //applications/blinky/main.cc */
// ...
#include "pw_system/rpc_server.h"
// ...
namespace pw::system {
void UserAppInit() {
// ...
pw::system::GetRpcServer().RegisterService(blinky_service);
}
} // namespace pw::system
Next steps
==========
That's the end of the quickstart! Learn more about ``pw_rpc``:
* Check out :ref:`module-pw_rpc-cpp` for detailed guidance on using the C++
client and server libraries.
* If you have any questions, you can talk to the Pigweed team in the ``#pw_rpc``
channel of our `Discord <https://discord.gg/M9NSeTA>`_.
* The rest of this page provides general guidance on common questions and use cases.
* The quickstart code was based off these real-world examples of the Pigweed
team adding ``pw_rpc`` to a project:
* `applications/blinky: Add Blinky RPC service <https://pwrev.dev/218225>`_
* `rpc: Use nanopb instead of pw_protobuf <https://pwrev.dev/218732>`_
* You can build clients in other languages, such as Python and TypeScript.
See :ref:`module-pw_rpc-libraries`.
.. _module-pw_rpc-zephyr:
---------------------------
Setting up pw_rpc in Zephyr
---------------------------
To enable ``pw_rpc.*`` for Zephyr add ``CONFIG_PIGWEED_RPC=y`` to the project's
configuration. This will enable the Kconfig menu for the following:
* ``pw_rpc.server`` which can be enabled via ``CONFIG_PIGWEED_RPC_SERVER=y``.
* ``pw_rpc.client`` which can be enabled via ``CONFIG_PIGWEED_RPC_CLIENT=y``.
* ``pw_rpc.client_server`` which can be enabled via
``CONFIG_PIGWEED_RPC_CLIENT_SERVER=y``.
* ``pw_rpc.common`` which can be enabled via ``CONFIG_PIGWEED_RPC_COMMON=y``.
.. _module-pw_rpc-syntax-versions:
---------------------------
proto2 versus proto3 syntax
---------------------------
Always use proto3 syntax rather than proto2 for new protocol buffers. proto2
protobufs can be compiled for ``pw_rpc``, but they are not as well supported
as proto3. Specifically, ``pw_rpc`` lacks support for non-zero default values
in proto2. When using Nanopb with ``pw_rpc``, proto2 response protobufs with
non-zero field defaults should be manually initialized to the default struct.
In the past, proto3 was sometimes avoided because it lacked support for field
presence detection. Fortunately, this has been fixed: proto3 now supports
``optional`` fields, which are equivalent to proto2 ``optional`` fields.
If you need to distinguish between a default-valued field and a missing field,
mark the field as ``optional``. The presence of the field can be detected
with ``std::optional``, a ``HasField(name)``, or ``has_<field>`` member,
depending on the library.
Optional fields have some overhead. If using Nanopb, default-valued fields
are included in the encoded proto, and the proto structs have a
``has_<field>`` flag for each optional field. Use plain fields if field
presence detection is not needed.
.. code-block:: protobuf
syntax = "proto3";
message MyMessage {
// Leaving this field unset is equivalent to setting it to 0.
int32 number = 1;
// Setting this field to 0 is different from leaving it unset.
optional int32 other_number = 2;
}
.. _module-pw_rpc-guides-headers:
-----------------------------------------------------------
When to use raw, Nanopb, or pw_protobuf headers and methods
-----------------------------------------------------------
There are three types of generated headers and methods available:
* Raw
* Nanopb
* ``pw_protobuf``
This section explains when to use each one. See
:ref:`module-pw_rpc-quickstart-codegen` for context.
``pw_rpc`` doesn't generate raw headers unless you specifically request them
in your build. These headers allow you to use raw methods. Raw methods only
give you a serialized request buffer and an output buffer. Projects typically
only work with raw headers and methods when they have large, complex proto
definitions (e.g. lots of callbacks) that are difficult to work with. Advanced
projects might use raw headers and methods when they need finer control over
how a proto is encoded.
Nanopb and ``pw_protobuf`` are higher-level libraries that make it easier
to serialize or deserialize protos inside raw bytes. Most new projects should
default to Nanopb for the time being. Pigweed has plans to improve ``pw_protobuf``
but those plans will take a while to implement.
The Nanopb and ``pw_protobuf`` APIs and codegen are both built on top of the
underlying raw APIs, which is why it's always possible to fallback to
raw APIs. If you define a Nanopb or ``pw_protobuf`` service, you can choose to
make individual methods raw by defining them using the raw method signature.
You still import the Nanopb or ``pw_protobuf`` header and can use the
methods from those libraries elsewhere. Unless you believe your entire service
requires pure raw methods, it's better to use Nanopb or ``pw_protobuf`` for
most things and fallback to raw only when needed.
.. caution:: Mixing Nanopb and pw_protobuf within the same service not supported
You can have a mix of Nanopb, ``pw_protobuf``, and raw services on the
same server. Within a service, you can mix raw and Nanopb or raw and ``pw_protobuf``
methods. You can't currently mix Nanopb and ``pw_protobuf`` methods but Pigweed
can implement this if needed. :bug:`234874320` outlines some conflicts you may
encounter if you try to include Nanopb and ``pw_protobuf`` headers in the
same source file.
.. _module-pw_rpc-guides-raw-fallback:
Falling back to raw methods
===========================
When implementing an RPC service using Nanopb or ``pw_protobuf``, you may
sometimes run into limitations of the protobuf library when used in conjunction
with ``pw_rpc``. For example, fields which use callbacks require those callbacks
to be set prior to the decode operation, but ``pw_rpc`` internally decodes every
message passed into a method implementation without any opportunity to set
these. Alternatively, you may simply want finer control over how your messages
are encoded.
To assist with these cases, ``pw_rpc`` allows any method within a Nanopb or
``pw_protobuf`` service to use its raw APIs without having to define the entire
service as raw. Implementors may choose on a method-by-method basis where they
desire to have access to the raw protobuf messages.
To implement a method using the raw APIs, all you have to do is change the
signature of the function --- ``pw_rpc`` will automatically handle the rest.
Examples are provided below, each showing a Nanopb method and its equivalent
raw signature.
Unary method
------------
When defining a unary method using the raw APIs, it is important to note that
there is no synchronous raw unary API. The asynchronous unary method signature
must be used instead.
**Nanopb**
.. code-block:: c++
// Synchronous unary method.
pw::Status DoFoo(const FooRequest& request, FooResponse response);
// Asynchronous unary method.
void DoFoo(const FooRequest& request,
pw::rpc::NanopbUnaryResponder<FooResponse>& responder);
**Raw**
.. code-block:: c++
// Only asynchronous unary methods are supported.
void DoFoo(pw::ConstByteSpan request, pw::rpc::RawUnaryResponder& responder);
Server streaming method
-----------------------
**Nanopb**
.. code-block:: c++
void DoFoo(const FooRequest& request,
pw::rpc::NanopbServerWriter<FooResponse>& writer);
**Raw**
.. code-block:: c++
void DoFoo(pw::ConstByteSpan request, pw::rpc::RawServerWriter& writer);
Client streaming method
-----------------------
**Nanopb**
.. code-block:: c++
void DoFoo(pw::rpc::NanopbServerReader<FooRequest, FooResponse>&);
**Raw**
.. code-block:: c++
void DoFoo(RawServerReader&);
Bidirectional streaming method
------------------------------
**Nanopb**
.. code-block:: c++
void DoFoo(pw::rpc::NanopbServerReaderWriter<Request, Response>&);
**Raw**
.. code-block:: c++
void DoFoo(RawServerReaderWriter&);
----------------------------
Testing a pw_rpc integration
----------------------------
After setting up a ``pw_rpc`` server in your project, you can test that it is
working as intended by registering the provided ``EchoService``, defined in
``echo.proto``, which echoes back a message that it receives.
.. literalinclude:: echo.proto
:language: protobuf
:lines: 14-
For example, in C++ with pw_protobuf:
.. code-block:: c++
#include "pw_rpc/server.h"
// Include the apporpriate header for your protobuf library.
#include "pw_rpc/echo_service_pwpb.h"
constexpr pw::rpc::Channel kChannels[] = { /* ... */ };
static pw::rpc::Server server(kChannels);
static pw::rpc::EchoService echo_service;
void Init() {
server.RegisterService(echo_service);
}
See :ref:`module-pw_rpc-cpp-testing` for more C++-specific testing
guidance.
-------------------------------
Benchmarking and stress testing
-------------------------------
``pw_rpc`` provides an RPC service and Python module for stress testing and
benchmarking a ``pw_rpc`` deployment.
pw_rpc provides tools for stress testing and benchmarking a Pigweed RPC
deployment and the transport it is running over. Two components are included:
* The pw.rpc.Benchmark service and its implementation.
* A Python module that runs tests using the Benchmark service.
pw.rpc.Benchmark service
========================
The Benchmark service provides a low-level RPC service for sending data between
the client and server. The service is defined in ``pw_rpc/benchmark.proto``.
A raw RPC implementation of the benchmark service is provided. This
implementation is suitable for use in any system with pw_rpc. To access this
service, add a dependency on ``"$dir_pw_rpc:benchmark"`` in GN or
``pw_rpc.benchmark`` in CMake. Then, include the service
(``#include "pw_rpc/benchmark.h"``), instantiate it, and register it with your
RPC server, like any other RPC service.
The Benchmark service was designed with the Python-based benchmarking tools in
mind, but it may be used directly to test basic RPC functionality. The service
is well suited for use in automated integration tests or in an interactive
console.
Benchmark service
-----------------
.. literalinclude:: benchmark.proto
:language: protobuf
:lines: 14-
Example
-------
.. code-block:: c++
#include "pw_rpc/benchmark.h"
#include "pw_rpc/server.h"
constexpr pw::rpc::Channel kChannels[] = { /* ... */ };
static pw::rpc::Server server(kChannels);
static pw::rpc::BenchmarkService benchmark_service;
void RegisterServices() {
server.RegisterService(benchmark_service);
}
Stress testing
==============
.. attention::
This section is experimental and liable to change.
The Benchmark service is also used as part of a stress test of the ``pw_rpc``
module. This stress test is implemented as an unguided fuzzer that uses
multiple worker threads to perform generated sequences of actions using RPC
``Call`` objects. The test is included as an integration test, and can found and
be run locally using GN:
.. code-block:: bash
$ gn desc out //:integration_tests deps | grep fuzz
//pw_rpc/fuzz:cpp_client_server_fuzz_test(//targets/host/pigweed_internal:pw_strict_host_clang_debug)
$ gn outputs out '//pw_rpc/fuzz:cpp_client_server_fuzz_test(//targets/host/pigweed_internal:pw_strict_host_clang_debug)'
pw_strict_host_clang_debug/gen/pw_rpc/fuzz/cpp_client_server_fuzz_test.pw_pystamp
$ ninja -C out pw_strict_host_clang_debug/gen/pw_rpc/fuzz/cpp_client_server_fuzz_test.pw_pystamp