1 // Copyright 2020 The Pigweed Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 // use this file except in compliance with the License. You may obtain a copy of 5 // the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 // License for the specific language governing permissions and limitations under 13 // the License. 14 #pragma once 15 16 #include <tuple> 17 #include <utility> 18 19 #include "pw_assert/assert.h" 20 #include "pw_bytes/span.h" 21 #include "pw_containers/vector.h" 22 #include "pw_preprocessor/arguments.h" 23 #include "pw_rpc/internal/hash.h" 24 #include "pw_rpc/internal/method_lookup.h" 25 #include "pw_rpc/internal/test_method_context.h" 26 #include "pw_rpc/nanopb/fake_channel_output.h" 27 #include "pw_rpc/nanopb/internal/method.h" 28 29 namespace pw::rpc { 30 31 // Declares a context object that may be used to invoke an RPC. The context is 32 // declared with the name of the implemented service and the method to invoke. 33 // The RPC can then be invoked with the call method. 34 // 35 // For a unary RPC, context.call(request) returns the status, and the response 36 // struct can be accessed via context.response(). 37 // 38 // PW_NANOPB_TEST_METHOD_CONTEXT(my::CoolService, TheMethod) context; 39 // EXPECT_EQ(OkStatus(), context.call({.some_arg = 123})); 40 // EXPECT_EQ(500, context.response().some_response_value); 41 // 42 // For a unary RPC with repeated fields in the response, nanopb uses a 43 // pb_callback_t field called when parsing the response as many times as the 44 // field is present in the protobuf. To set the pb_callback_t fields create the 45 // Response struct and pass it to the response method: 46 // 47 // PW_NANOPB_TEST_METHOD_CONTEXT(my::CoolService, TheMethod) context; 48 // EXPECT_EQ(OkStatus(), context.call({.some_arg = 123})); 49 // 50 // TheMethodResponse response{}; 51 // response.repeated_field.funcs.decode = +[](pb_istream_t* stream, 52 // const pb_field_iter_t* field, 53 // void** arg) -> bool { 54 // ... decode the field from stream with pb_decode* functions ... 55 // EXPECT_EQ(submsg.some_field, 123); 56 // return true; 57 // }; 58 // response.repeated_field.arg = &some_context_passed_decode_if_needed; 59 // context.response(response); // Callbacks called from here. 60 // 61 // For a server streaming RPC, context.call(request) invokes the method. As in a 62 // normal RPC, the method completes when the ServerWriter's Finish method is 63 // called (or it goes out of scope). 64 // 65 // PW_NANOPB_TEST_METHOD_CONTEXT(my::CoolService, TheStreamingMethod) context; 66 // context.call({.some_arg = 123}); 67 // 68 // EXPECT_TRUE(context.done()); // Check that the RPC completed 69 // EXPECT_EQ(OkStatus(), context.status()); // Check the status 70 // 71 // EXPECT_EQ(3u, context.responses().size()); 72 // EXPECT_EQ(123, context.responses()[0].value); // check individual responses 73 // 74 // for (const MyResponse& response : context.responses()) { 75 // // iterate over the responses 76 // } 77 // 78 // PW_NANOPB_TEST_METHOD_CONTEXT forwards its constructor arguments to the 79 // underlying serivce. For example: 80 // 81 // PW_NANOPB_TEST_METHOD_CONTEXT(MyService, Go) context(service, args); 82 // 83 // PW_NANOPB_TEST_METHOD_CONTEXT takes one optional argument: 84 // 85 // size_t kMaxPackets: maximum packets to store 86 // 87 // Example: 88 // 89 // PW_NANOPB_TEST_METHOD_CONTEXT(MyService, BestMethod, 3, 256) context; 90 // ASSERT_EQ(3u, context.responses().max_size()); 91 // 92 #define PW_NANOPB_TEST_METHOD_CONTEXT(service, method, ...) \ 93 ::pw::rpc::NanopbTestMethodContext<service, \ 94 &service::method, \ 95 ::pw::rpc::internal::Hash(#method) \ 96 PW_COMMA_ARGS(__VA_ARGS__)> 97 template <typename Service, 98 auto kMethod, 99 uint32_t kMethodId, 100 size_t kMaxPackets = 6, 101 size_t kPayloadsBufferSizeBytes = 256> 102 class NanopbTestMethodContext; 103 104 // Internal classes that implement NanopbTestMethodContext. 105 namespace internal::test::nanopb { 106 107 // Collects everything needed to invoke a particular RPC. 108 template <typename Service, 109 auto kMethod, 110 uint32_t kMethodId, 111 size_t kMaxPackets, 112 size_t kPayloadsBufferSizeBytes> 113 class NanopbInvocationContext 114 : public InvocationContext< 115 NanopbFakeChannelOutput<kMaxPackets, kPayloadsBufferSizeBytes>, 116 Service, 117 kMethodId> { 118 public: 119 using Request = internal::Request<kMethod>; 120 using Response = internal::Response<kMethod>; 121 122 // Gives access to the RPC's most recent response. response()123 Response response() const { 124 Response response{}; 125 PW_ASSERT_OK(kMethodInfo.serde().response().Decode(Base::responses().back(), 126 response)); 127 return response; 128 } 129 130 // Gives access to the RPC's most recent response using pased Response object 131 // to parse the nanopb. Use this version when you need to set pb_callback_t 132 // fields in the Response object before parsing. response(Response & response)133 void response(Response& response) const { 134 PW_ASSERT_OK(kMethodInfo.serde().response().Decode(Base::responses().back(), 135 response)); 136 } 137 responses()138 NanopbPayloadsView<Response> responses() const { 139 return Base::output().template payload_structs<Response>( 140 kMethodInfo.serde().response(), 141 MethodTraits<decltype(kMethod)>::kType, 142 Base::channel_id(), 143 internal::UnwrapServiceId(Base::service().service_id()), 144 kMethodId); 145 } 146 147 protected: 148 template <typename... Args> NanopbInvocationContext(Args &&...args)149 NanopbInvocationContext(Args&&... args) 150 : Base(kMethodInfo, 151 MethodTraits<decltype(kMethod)>::kType, 152 std::forward<Args>(args)...) {} 153 154 template <size_t kEncodingBufferSizeBytes = 128> SendClientStream(const Request & request)155 void SendClientStream(const Request& request) PW_LOCKS_EXCLUDED(rpc_lock()) { 156 std::array<std::byte, kEncodingBufferSizeBytes> buffer; 157 Base::SendClientStream(span(buffer).first( 158 kMethodInfo.serde().request().Encode(&request, buffer).size())); 159 } 160 161 private: 162 using Base = InvocationContext< 163 NanopbFakeChannelOutput<kMaxPackets, kPayloadsBufferSizeBytes>, 164 Service, 165 kMethodId>; 166 167 static constexpr NanopbMethod kMethodInfo = 168 MethodLookup::GetNanopbMethod<Service, kMethodId>(); 169 }; 170 171 // Method invocation context for a unary RPC. Returns the status in 172 // call_context() and provides the response through the response() method. 173 template <typename Service, 174 auto kMethod, 175 uint32_t kMethodId, 176 size_t kPayloadsBufferSizeBytes> 177 class UnaryContext : public NanopbInvocationContext<Service, 178 kMethod, 179 kMethodId, 180 1, 181 kPayloadsBufferSizeBytes> { 182 private: 183 using Base = NanopbInvocationContext<Service, 184 kMethod, 185 kMethodId, 186 1, 187 kPayloadsBufferSizeBytes>; 188 189 public: 190 using Request = typename Base::Request; 191 using Response = typename Base::Response; 192 193 template <typename... Args> UnaryContext(Args &&...args)194 UnaryContext(Args&&... args) : Base(std::forward<Args>(args)...) {} 195 196 // Invokes the RPC with the provided request. Returns the status. call(const Request & request)197 auto call(const Request& request) { 198 if constexpr (MethodTraits<decltype(kMethod)>::kSynchronous) { 199 Base::output().clear(); 200 201 NanopbUnaryResponder<Response> responder = 202 Base::template GetResponder<NanopbUnaryResponder<Response>>(); 203 Response response = {}; 204 Status status = 205 CallMethodImplFunction<kMethod>(Base::service(), request, response); 206 PW_ASSERT(responder.Finish(response, status).ok()); 207 return status; 208 209 } else { 210 Base::template call<kMethod, NanopbUnaryResponder<Response>>(request); 211 } 212 } 213 }; 214 215 // Method invocation context for a server streaming RPC. 216 template <typename Service, 217 auto kMethod, 218 uint32_t kMethodId, 219 size_t kMaxPackets, 220 size_t kPayloadsBufferSizeBytes> 221 class ServerStreamingContext 222 : public NanopbInvocationContext<Service, 223 kMethod, 224 kMethodId, 225 kMaxPackets, 226 kPayloadsBufferSizeBytes> { 227 private: 228 using Base = NanopbInvocationContext<Service, 229 kMethod, 230 kMethodId, 231 kMaxPackets, 232 kPayloadsBufferSizeBytes>; 233 234 public: 235 using Request = typename Base::Request; 236 using Response = typename Base::Response; 237 238 template <typename... Args> ServerStreamingContext(Args &&...args)239 ServerStreamingContext(Args&&... args) : Base(std::forward<Args>(args)...) {} 240 241 // Invokes the RPC with the provided request. call(const Request & request)242 void call(const Request& request) { 243 Base::template call<kMethod, NanopbServerWriter<Response>>(request); 244 } 245 246 // Returns a server writer which writes responses into the context's buffer. 247 // This should not be called alongside call(); use one or the other. writer()248 NanopbServerWriter<Response> writer() { 249 return Base::template GetResponder<NanopbServerWriter<Response>>(); 250 } 251 }; 252 253 // Method invocation context for a client streaming RPC. 254 template <typename Service, 255 auto kMethod, 256 uint32_t kMethodId, 257 size_t kMaxPackets, 258 size_t kPayloadsBufferSizeBytes> 259 class ClientStreamingContext 260 : public NanopbInvocationContext<Service, 261 kMethod, 262 kMethodId, 263 kMaxPackets, 264 kPayloadsBufferSizeBytes> { 265 private: 266 using Base = NanopbInvocationContext<Service, 267 kMethod, 268 kMethodId, 269 kMaxPackets, 270 kPayloadsBufferSizeBytes>; 271 272 public: 273 using Request = typename Base::Request; 274 using Response = typename Base::Response; 275 276 template <typename... Args> ClientStreamingContext(Args &&...args)277 ClientStreamingContext(Args&&... args) : Base(std::forward<Args>(args)...) {} 278 279 // Invokes the RPC. call()280 void call() { 281 Base::template call<kMethod, NanopbServerReader<Request, Response>>(); 282 } 283 284 // Returns a server reader which writes responses into the context's buffer. 285 // This should not be called alongside call(); use one or the other. reader()286 NanopbServerReader<Request, Response> reader() { 287 return Base::template GetResponder<NanopbServerReader<Request, Response>>(); 288 } 289 290 // Allow sending client streaming packets. 291 using Base::SendClientStream; 292 using Base::SendClientStreamEnd; 293 }; 294 295 // Method invocation context for a bidirectional streaming RPC. 296 template <typename Service, 297 auto kMethod, 298 uint32_t kMethodId, 299 size_t kMaxPackets, 300 size_t kPayloadsBufferSizeBytes> 301 class BidirectionalStreamingContext 302 : public NanopbInvocationContext<Service, 303 kMethod, 304 kMethodId, 305 kMaxPackets, 306 kPayloadsBufferSizeBytes> { 307 private: 308 using Base = NanopbInvocationContext<Service, 309 kMethod, 310 kMethodId, 311 kMaxPackets, 312 kPayloadsBufferSizeBytes>; 313 314 public: 315 using Request = typename Base::Request; 316 using Response = typename Base::Response; 317 318 template <typename... Args> BidirectionalStreamingContext(Args &&...args)319 BidirectionalStreamingContext(Args&&... args) 320 : Base(std::forward<Args>(args)...) {} 321 322 // Invokes the RPC. call()323 void call() { 324 Base::template call<kMethod, NanopbServerReaderWriter<Request, Response>>(); 325 } 326 327 // Returns a server reader which writes responses into the context's buffer. 328 // This should not be called alongside call(); use one or the other. reader_writer()329 NanopbServerReaderWriter<Request, Response> reader_writer() { 330 return Base::template GetResponder< 331 NanopbServerReaderWriter<Request, Response>>(); 332 } 333 334 // Allow sending client streaming packets. 335 using Base::SendClientStream; 336 using Base::SendClientStreamEnd; 337 }; 338 339 // Alias to select the type of the context object to use based on which type of 340 // RPC it is for. 341 template <typename Service, 342 auto kMethod, 343 uint32_t kMethodId, 344 size_t kMaxPackets, 345 size_t kPayloadsBufferSizeBytes> 346 using Context = std::tuple_element_t< 347 static_cast<size_t>(internal::MethodTraits<decltype(kMethod)>::kType), 348 std::tuple< 349 UnaryContext<Service, kMethod, kMethodId, kPayloadsBufferSizeBytes>, 350 ServerStreamingContext<Service, 351 kMethod, 352 kMethodId, 353 kMaxPackets, 354 kPayloadsBufferSizeBytes>, 355 ClientStreamingContext<Service, 356 kMethod, 357 kMethodId, 358 kMaxPackets, 359 kPayloadsBufferSizeBytes>, 360 BidirectionalStreamingContext<Service, 361 kMethod, 362 kMethodId, 363 kMaxPackets, 364 kPayloadsBufferSizeBytes>>>; 365 366 } // namespace internal::test::nanopb 367 368 template <typename Service, 369 auto kMethod, 370 uint32_t kMethodId, 371 size_t kMaxPackets, 372 size_t kPayloadsBufferSizeBytes> 373 class NanopbTestMethodContext 374 : public internal::test::nanopb::Context<Service, 375 kMethod, 376 kMethodId, 377 kMaxPackets, 378 kPayloadsBufferSizeBytes> { 379 public: 380 // Forwards constructor arguments to the service class. 381 template <typename... ServiceArgs> NanopbTestMethodContext(ServiceArgs &&...service_args)382 NanopbTestMethodContext(ServiceArgs&&... service_args) 383 : internal::test::nanopb::Context<Service, 384 kMethod, 385 kMethodId, 386 kMaxPackets, 387 kPayloadsBufferSizeBytes>( 388 std::forward<ServiceArgs>(service_args)...) {} 389 }; 390 391 } // namespace pw::rpc 392