xref: /aosp_15_r20/external/pigweed/pw_rpc/guides.rst (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1.. _module-pw_rpc-guides:
2
3===================
4Quickstart & guides
5===================
6.. pigweed-module-subpage::
7   :name: pw_rpc
8
9.. _module-pw_rpc-quickstart:
10
11----------
12Quickstart
13----------
14This section guides you through the process of adding ``pw_rpc`` to your project.
15
16Check out :ref:`module-pw_rpc-design-overview` for a conceptual explanation
17of the RPC lifecycle.
18
191. RPC service declaration
20==========================
21Declare a :ref:`service <module-pw_rpc-design-services>` in a service
22definition:
23
24.. code-block:: protobuf
25
26   /* //applications/blinky/blinky.proto */
27
28   syntax = "proto3";
29
30   package blinky;
31
32   import "pw_protobuf_protos/common.proto";
33
34   service Blinky {
35     // Toggles the LED on or off.
36     rpc ToggleLed(pw.protobuf.Empty) returns (pw.protobuf.Empty);
37     // Continuously blinks the board LED a specified number of times.
38     rpc Blink(BlinkRequest) returns (pw.protobuf.Empty);
39   }
40
41   message BlinkRequest {
42     // The interval at which to blink the LED, in milliseconds.
43     uint32 interval_ms = 1;
44     // The number of times to blink the LED.
45     optional uint32 blink_count = 2;
46   } // BlinkRequest
47
48Declare the protocol buffer in ``BUILD.bazel``:
49
50.. code-block:: python
51
52   # //applications/blinky/BUILD.bazel
53
54   load(
55       "@pigweed//pw_protobuf_compiler:pw_proto_library.bzl",
56       "nanopb_proto_library",
57       "nanopb_rpc_proto_library",
58   )
59
60   proto_library(
61       name = "proto",
62       srcs = ["blinky.proto"],
63       deps = [
64           "@pigweed//pw_protobuf:common_proto",
65       ],
66   )
67
68   nanopb_proto_library(
69       name = "nanopb",
70       deps = [":proto"],
71   )
72
73   nanopb_rpc_proto_library(
74       name = "nanopb_rpc",
75       nanopb_proto_library_deps = [":nanopb"],
76       deps = [":proto"],
77   )
78
79.. _module-pw_rpc-quickstart-codegen:
80
812. RPC code generation
82======================
83How do you interact with this protobuf from C++? ``pw_rpc`` uses
84codegen libraries to automatically transform ``.proto`` definitions
85into C++ header files. These headers are generated in the build
86output directory. Their exact location varies by build system and toolchain
87but the C++ include path always matches the source declarations in
88``proto_library``. The ``.proto`` extension is replaced with an extension
89corresponding to the protobuf codegen library in use:
90
91.. csv-table::
92   :header: "Protobuf libraries", "Build subtarget", "Protobuf header", "pw_rpc header"
93
94   "Raw only", "``.raw_rpc``", "(none)", "``.raw_rpc.pb.h``"
95   "Nanopb or raw", "``.nanopb_rpc``", "``.pb.h``", "``.rpc.pb.h``"
96   "pw_protobuf or raw", "``.pwpb_rpc``", "``.pwpb.h``", "``.rpc.pwpb.h``"
97
98Most projects should default to Nanopb. See
99:ref:`module-pw_rpc-guides-headers`.
100
101For example, the generated RPC header for ``applications/blinky.proto`` is
102``applications/blinky.rpc.pb.h`` for Nanopb or
103``applications/blinky.raw_rpc.pb.h`` for raw RPCs.
104
105The generated header defines a base class for each RPC service declared in the
106``.proto`` file. A service named ``TheService`` in package ``foo.bar`` would
107generate the following base class for ``pw_protobuf``:
108
109.. cpp:class:: template <typename Implementation> foo::bar::pw_rpc::pwpb::TheService::Service
110
1113. RPC service definition
112=========================
113Implement a service class by inheriting from the generated RPC service
114base class and defining a method for each RPC. The methods must match the name
115and function signature for one of the supported protobuf implementations.
116Services may mix and match protobuf implementations within one service.
117
118A Nanopb implementation of the ``Blinky`` service looks like this:
119
120.. code-block:: cpp
121
122   /* //applications/blinky/main.cc */
123
124   // ...
125   #include "applications/blinky/blinky.rpc.pb.h"
126   #include "pw_system/rpc_server.h"
127   // ...
128
129   class BlinkyService final
130       : public blinky::pw_rpc::nanopb::Blinky::Service<BlinkyService> {
131   public:
132     pw::Status ToggleLed(const pw_protobuf_Empty &, pw_protobuf_Empty &) {
133       // Turn the LED off if it's currently on and vice versa
134     }
135     pw::Status Blink(const blinky_BlinkRequest &request, pw_protobuf_Empty &) {
136       if (request.blink_count == 0) {
137         // Stop blinking
138       }
139       if (request.interval_ms > 0) {
140         // Change the blink interval
141       }
142       if (request.has_blink_count) {  // Auto-generated property
143         // Blink request.blink_count times
144       }
145     }
146   };
147
148   BlinkyService blinky_service;
149
150   namespace pw::system {
151
152   void UserAppInit() {
153     // ...
154     pw::system::GetRpcServer().RegisterService(blinky_service);
155   }
156
157   } // namespace pw::system
158
159Declare the implementation in ``BUILD.bazel``:
160
161.. code-block:: python
162
163   # //applications/blinky/BUILD.bazel
164
165   cc_binary(
166       name = "blinky",
167       srcs = ["main.cc"],
168       deps = [
169           ":nanopb_rpc",
170           # ...
171           "@pigweed//pw_system",
172       ],
173   )
174
175.. tip::
176
177   The generated code includes RPC service implementation stubs. You can
178   reference or copy and paste these to get started with implementing a service.
179   These stub classes are generated at the bottom of the pw_rpc proto header.
180
181   To use the stubs, do the following:
182
183   #. Locate the generated RPC header in the build directory. For example:
184
185      .. code-block:: sh
186
187         cd bazel-out && find . -name blinky.rpc.pb.h
188
189   #. Scroll to the bottom of the generated RPC header.
190   #. Copy the stub class declaration to a header file.
191   #. Copy the member function definitions to a source file.
192   #. Rename the class or change the namespace, if desired.
193   #. List these files in a build target with a dependency on
194      ``proto_library``.
195
1964. Register the service with a server
197=====================================
198Set up the server and register the service:
199
200.. code-block:: cpp
201
202   /* //applications/blinky/main.cc */
203
204   // ...
205   #include "pw_system/rpc_server.h"
206   // ...
207
208   namespace pw::system {
209
210   void UserAppInit() {
211     // ...
212     pw::system::GetRpcServer().RegisterService(blinky_service);
213   }
214
215   } // namespace pw::system
216
217Next steps
218==========
219That's the end of the quickstart! Learn more about ``pw_rpc``:
220
221* Check out :ref:`module-pw_rpc-cpp` for detailed guidance on using the C++
222  client and server libraries.
223
224* If you have any questions, you can talk to the Pigweed team in the ``#pw_rpc``
225  channel of our `Discord <https://discord.gg/M9NSeTA>`_.
226
227* The rest of this page provides general guidance on common questions and use cases.
228
229* The quickstart code was based off these real-world examples of the Pigweed
230  team adding ``pw_rpc`` to a project:
231
232  * `applications/blinky: Add Blinky RPC service <https://pwrev.dev/218225>`_
233
234  * `rpc: Use nanopb instead of pw_protobuf <https://pwrev.dev/218732>`_
235
236* You can build clients in other languages, such as Python and TypeScript.
237  See :ref:`module-pw_rpc-libraries`.
238
239.. _module-pw_rpc-zephyr:
240
241---------------------------
242Setting up pw_rpc in Zephyr
243---------------------------
244To enable ``pw_rpc.*`` for Zephyr add ``CONFIG_PIGWEED_RPC=y`` to the project's
245configuration. This will enable the Kconfig menu for the following:
246
247* ``pw_rpc.server`` which can be enabled via ``CONFIG_PIGWEED_RPC_SERVER=y``.
248* ``pw_rpc.client`` which can be enabled via ``CONFIG_PIGWEED_RPC_CLIENT=y``.
249* ``pw_rpc.client_server`` which can be enabled via
250  ``CONFIG_PIGWEED_RPC_CLIENT_SERVER=y``.
251* ``pw_rpc.common`` which can be enabled via ``CONFIG_PIGWEED_RPC_COMMON=y``.
252
253.. _module-pw_rpc-syntax-versions:
254
255---------------------------
256proto2 versus proto3 syntax
257---------------------------
258Always use proto3 syntax rather than proto2 for new protocol buffers. proto2
259protobufs can be compiled for ``pw_rpc``, but they are not as well supported
260as proto3. Specifically, ``pw_rpc`` lacks support for non-zero default values
261in proto2. When using Nanopb with ``pw_rpc``, proto2 response protobufs with
262non-zero field defaults should be manually initialized to the default struct.
263
264In the past, proto3 was sometimes avoided because it lacked support for field
265presence detection. Fortunately, this has been fixed: proto3 now supports
266``optional`` fields, which are equivalent to proto2 ``optional`` fields.
267
268If you need to distinguish between a default-valued field and a missing field,
269mark the field as ``optional``. The presence of the field can be detected
270with ``std::optional``, a ``HasField(name)``, or ``has_<field>`` member,
271depending on the library.
272
273Optional fields have some overhead. If using Nanopb, default-valued fields
274are included in the encoded proto, and the proto structs have a
275``has_<field>`` flag for each optional field. Use plain fields if field
276presence detection is not needed.
277
278.. code-block:: protobuf
279
280   syntax = "proto3";
281
282   message MyMessage {
283     // Leaving this field unset is equivalent to setting it to 0.
284     int32 number = 1;
285
286     // Setting this field to 0 is different from leaving it unset.
287     optional int32 other_number = 2;
288   }
289
290.. _module-pw_rpc-guides-headers:
291
292-----------------------------------------------------------
293When to use raw, Nanopb, or pw_protobuf headers and methods
294-----------------------------------------------------------
295There are three types of generated headers and methods available:
296
297* Raw
298* Nanopb
299* ``pw_protobuf``
300
301This section explains when to use each one. See
302:ref:`module-pw_rpc-quickstart-codegen` for context.
303
304``pw_rpc`` doesn't generate raw headers unless you specifically request them
305in your build. These headers allow you to use raw methods. Raw methods only
306give you a serialized request buffer and an output buffer. Projects typically
307only work with raw headers and methods when they have large, complex proto
308definitions (e.g. lots of callbacks) that are difficult to work with. Advanced
309projects might use raw headers and methods when they need finer control over
310how a proto is encoded.
311
312Nanopb and ``pw_protobuf`` are higher-level libraries that make it easier
313to serialize or deserialize protos inside raw bytes. Most new projects should
314default to Nanopb for the time being. Pigweed has plans to improve ``pw_protobuf``
315but those plans will take a while to implement.
316
317The Nanopb and ``pw_protobuf`` APIs and codegen are both built on top of the
318underlying raw APIs, which is why it's always possible to fallback to
319raw APIs. If you define a Nanopb or ``pw_protobuf`` service, you can choose to
320make individual methods raw by defining them using the raw method signature.
321You still import the Nanopb or ``pw_protobuf`` header and can use the
322methods from those libraries elsewhere. Unless you believe your entire service
323requires pure raw methods, it's better to use Nanopb or ``pw_protobuf`` for
324most things and fallback to raw only when needed.
325
326.. caution:: Mixing Nanopb and pw_protobuf within the same service not supported
327
328   You can have a mix of Nanopb, ``pw_protobuf``, and raw services on the
329   same server. Within a service, you can mix raw and Nanopb or raw and ``pw_protobuf``
330   methods. You can't currently mix Nanopb and ``pw_protobuf`` methods but Pigweed
331   can implement this if needed. :bug:`234874320` outlines some conflicts you may
332   encounter if you try to include Nanopb and ``pw_protobuf`` headers in the
333   same source file.
334
335.. _module-pw_rpc-guides-raw-fallback:
336
337Falling back to raw methods
338===========================
339When implementing an RPC service using Nanopb or ``pw_protobuf``, you may
340sometimes run into limitations of the protobuf library when used in conjunction
341with ``pw_rpc``. For example, fields which use callbacks require those callbacks
342to be set prior to the decode operation, but ``pw_rpc`` internally decodes every
343message passed into a method implementation without any opportunity to set
344these. Alternatively, you may simply want finer control over how your messages
345are encoded.
346
347To assist with these cases, ``pw_rpc`` allows any method within a Nanopb or
348``pw_protobuf`` service to use its raw APIs without having to define the entire
349service as raw. Implementors may choose on a method-by-method basis where they
350desire to have access to the raw protobuf messages.
351
352To implement a method using the raw APIs, all you have to do is change the
353signature of the function --- ``pw_rpc`` will automatically handle the rest.
354Examples are provided below, each showing a Nanopb method and its equivalent
355raw signature.
356
357Unary method
358------------
359When defining a unary method using the raw APIs, it is important to note that
360there is no synchronous raw unary API. The asynchronous unary method signature
361must be used instead.
362
363**Nanopb**
364
365.. code-block:: c++
366
367   // Synchronous unary method.
368   pw::Status DoFoo(const FooRequest& request, FooResponse response);
369
370   // Asynchronous unary method.
371   void DoFoo(const FooRequest& request,
372              pw::rpc::NanopbUnaryResponder<FooResponse>& responder);
373
374**Raw**
375
376.. code-block:: c++
377
378   // Only asynchronous unary methods are supported.
379   void DoFoo(pw::ConstByteSpan request, pw::rpc::RawUnaryResponder& responder);
380
381Server streaming method
382-----------------------
383
384**Nanopb**
385
386.. code-block:: c++
387
388   void DoFoo(const FooRequest& request,
389              pw::rpc::NanopbServerWriter<FooResponse>& writer);
390
391**Raw**
392
393.. code-block:: c++
394
395   void DoFoo(pw::ConstByteSpan request, pw::rpc::RawServerWriter& writer);
396
397Client streaming method
398-----------------------
399
400**Nanopb**
401
402.. code-block:: c++
403
404   void DoFoo(pw::rpc::NanopbServerReader<FooRequest, FooResponse>&);
405
406**Raw**
407
408.. code-block:: c++
409
410   void DoFoo(RawServerReader&);
411
412Bidirectional streaming method
413------------------------------
414
415**Nanopb**
416
417.. code-block:: c++
418
419   void DoFoo(pw::rpc::NanopbServerReaderWriter<Request, Response>&);
420
421**Raw**
422
423.. code-block:: c++
424
425   void DoFoo(RawServerReaderWriter&);
426
427----------------------------
428Testing a pw_rpc integration
429----------------------------
430After setting up a ``pw_rpc`` server in your project, you can test that it is
431working as intended by registering the provided ``EchoService``, defined in
432``echo.proto``, which echoes back a message that it receives.
433
434.. literalinclude:: echo.proto
435   :language: protobuf
436   :lines: 14-
437
438For example, in C++ with pw_protobuf:
439
440.. code-block:: c++
441
442   #include "pw_rpc/server.h"
443
444   // Include the apporpriate header for your protobuf library.
445   #include "pw_rpc/echo_service_pwpb.h"
446
447   constexpr pw::rpc::Channel kChannels[] = { /* ... */ };
448   static pw::rpc::Server server(kChannels);
449
450   static pw::rpc::EchoService echo_service;
451
452   void Init() {
453     server.RegisterService(echo_service);
454   }
455
456See :ref:`module-pw_rpc-cpp-testing` for more C++-specific testing
457guidance.
458
459-------------------------------
460Benchmarking and stress testing
461-------------------------------
462``pw_rpc`` provides an RPC service and Python module for stress testing and
463benchmarking a ``pw_rpc`` deployment.
464
465pw_rpc provides tools for stress testing and benchmarking a Pigweed RPC
466deployment and the transport it is running over. Two components are included:
467
468* The pw.rpc.Benchmark service and its implementation.
469* A Python module that runs tests using the Benchmark service.
470
471pw.rpc.Benchmark service
472========================
473The Benchmark service provides a low-level RPC service for sending data between
474the client and server. The service is defined in ``pw_rpc/benchmark.proto``.
475
476A raw RPC implementation of the benchmark service is provided. This
477implementation is suitable for use in any system with pw_rpc. To access this
478service, add a dependency on ``"$dir_pw_rpc:benchmark"`` in GN or
479``pw_rpc.benchmark`` in CMake. Then, include the service
480(``#include "pw_rpc/benchmark.h"``), instantiate it, and register it with your
481RPC server, like any other RPC service.
482
483The Benchmark service was designed with the Python-based benchmarking tools in
484mind, but it may be used directly to test basic RPC functionality. The service
485is well suited for use in automated integration tests or in an interactive
486console.
487
488Benchmark service
489-----------------
490.. literalinclude:: benchmark.proto
491  :language: protobuf
492  :lines: 14-
493
494Example
495-------
496.. code-block:: c++
497
498   #include "pw_rpc/benchmark.h"
499   #include "pw_rpc/server.h"
500
501   constexpr pw::rpc::Channel kChannels[] = { /* ... */ };
502   static pw::rpc::Server server(kChannels);
503
504   static pw::rpc::BenchmarkService benchmark_service;
505
506   void RegisterServices() {
507     server.RegisterService(benchmark_service);
508   }
509
510Stress testing
511==============
512.. attention::
513   This section is experimental and liable to change.
514
515The Benchmark service is also used as part of a stress test of the ``pw_rpc``
516module. This stress test is implemented as an unguided fuzzer that uses
517multiple worker threads to perform generated sequences of actions using RPC
518``Call`` objects. The test is included as an integration test, and can found and
519be run locally using GN:
520
521.. code-block:: bash
522
523   $ gn desc out //:integration_tests deps | grep fuzz
524   //pw_rpc/fuzz:cpp_client_server_fuzz_test(//targets/host/pigweed_internal:pw_strict_host_clang_debug)
525
526   $ gn outputs out '//pw_rpc/fuzz:cpp_client_server_fuzz_test(//targets/host/pigweed_internal:pw_strict_host_clang_debug)'
527   pw_strict_host_clang_debug/gen/pw_rpc/fuzz/cpp_client_server_fuzz_test.pw_pystamp
528
529   $ ninja -C out pw_strict_host_clang_debug/gen/pw_rpc/fuzz/cpp_client_server_fuzz_test.pw_pystamp
530