Introduction

The LRPC definition is written in YAML and therefore benefits from all the features and tooling that are available for YAML. Think about editor support, easy parsing in various programming languages. The fact that there is a schema available, makes it possible to have code completion and documentation in supporting editors.

Top level

The LRPC definition file has the following properties:

Required Optional
name structs
services enums
  constants
  rx_buffer_size
  tx_buffer_size
  namespace
  version

At the top level it is also allowed to use additional properties. These properties are ignored by the LRPC tool, but may be useful creating anchors or for any other purpose that you may have. Remember that it’s very easy to parse the definition file, so everyone is free to extend the functionality of LRPC.

Name

This is the name of the RPC engine. It is used in generated files and directories, as well as generated code. Therefore, the name must be a valid C++ identifier

Services

A single RPC engine can contain up to 256 different services. A service can be considered a group of related functions. In the generated code a service with functions corresponds to a class with methods. A RPC engine must have at least one service.

A service has the following properties

Required Optional
name id
  functions
  streams

name is the name of the service. It must be a valid C++ identifier. id is the service ID. When not explicitly specified, it is generated by LRPC. Although functions and streams are both optional, at least one of them must be present.

Service ID

Every LRPC service has an identifier that is needed for proper transfer of information between two endpoints. If the service identifier is not specified, LRPC will generate one. When starting out with a fresh RPC, it’s usually not necessary to specify service IDs. Later on it may be useful to explicitly specify a service ID for backwards compatibility.

NOTE: The most efficient code is generated when service IDs are contiguous and start at 0. This is the default when no IDs are specified

Functions

A single LRPC service must contain at least one function or stream and up to 256 (combined). A function can have any number of arguments and return values.

A function has the following properties:

Required Optional
name id
  params
  returns

name is the name of the function. It must be a valid C++ identifier. id is the function identifier, similar to the service ID. params is a list of parameters and returns is a list of return values. Every item in params and returns is a LrpcType.

Streams

Streams are similar to functions, but they don’t have any return values. In fact, there is no response to stream data at all. Stream data can consist of any number of items. Data can be streamed from client to server of vice versa (determined by origin), but a data stream is always initiated by the client. A stream while params in a function are always from client to server, in a stream the direction of params depends on the direction of the stream.

A stream has the following properties:

Required Optional
name id
origin params
  finite

name is the name of the stream. It must be a valid C++ identifier. origin determines the direction of the stream. It can be either client or server. id is the stream identifier, similar to the service ID. params is a list of parameters. Every item in params is a LrpcType.

Sometimes a stream can produce an infinite amount of messages, for example a sensor data stream from server to client. In this case the client starts the stream and stops the stream when needed. In other cases a stream is limited by design, for example retrieving all log messages stored on a device. In this case it’s useful for the receiving side to know when the last message has been received. LRPC can help in this situation if the finite property is set to true, but it does come at a small cost. Every message gets an implicit boolean parameter (one byte) that is only true for the final message. The LRPC client CLI uses this information to gracefully terminate a streaming session

When a service contains both functions and streams, automatic ID assignment depends on which is specified first. A few examples:

A definition first specifies two functions in a service and then two streams. No explicit IDs are given. The functions will receive IDs 0 and 1 and the streams will receive IDs 2 and 3.

A definition first specifies two streams in a service and then two function. The second stream has an explicit ID of 55. LRPC generates the other IDs and the result are: first stream has ID 0, second stream has ID 55. First function has ID 56, second function has ID 57.

A definition specifies a service with three functions. The first function has an explicit ID of 20, the second function has an explicit ID of 19, the third function does not have an explicit ID. LRPC generates an error because the generated ID for the third function is 20, resulting in duplicate function IDs.

Structs

LRPC supports defining custom aggregate data types in the structs property. structs contains a list of custom struct definitions, where every item has the following properties:

Required Optional
name external
fields external_namespace

name is the name of the struct. It must be a valid C++ identifier. fields is a list of data members, every member being a LrpcType. Custom structs can be referenced inside the LRPC definition file by prepending the name with the @ sign.

Enums

LRPC supports defining custom enum types in the enums property. enums contains a list of custom enum definitions, where every item has the following properties:

Required Optional
name external
fields external_namespace

name is the name of the enum. It must be a valid C++ identifier. fields is a list of enum fields, every field having a required name property and an optional id property. name is the enum label and id is the value of the label. fields can also simply be a list of strings, every string being the name of a field. The field IDs are determined automatically in that case. This allows for shorter notation in case the specific value of the IDs is not important.

It is possible to use an enum in LRPC that already exists in your codebase. In this case, the external property must be used to specify the file that contains the definition of the enum. If the external enum lives inside a namespace, that namespace must be specified with the external_namespace property. LRPC does not generate any functional code for external enums, but it does generate some checks to verify that the external enum corresponds to the LRPC definition file. LRPC also creates an alias for the external type (unless code is generated in the global namespace and the external enum lives in the global namespace). The alias is used internally in code generated by LRPC

Example:

...
enums:
  # short notation
  - { name: MyEnum, fields: [V0, V1, V2, V3]}
  # short notation, external enum in namespace
  - { name: MyEnum2, fields: [V0, V1, V2, V3], external: ext_files/MyEnum2.hpp, external_namespace: "a::b::c"}
  # full notation, external enum in global namespace
  - name: MyEnum3
    external: ext_files/MyEnum3.hpp
    fields:
      - {name: V0} # id omitted, defaults to 0
      - {name: V1} # id omitted, defaults to 1
      - {name: V55, id: 55}
      - {name: V200, id: 200 }
      - {name: V201} # id omitted, defaults to 201
...

Constants

LRPC supports defining constants in the constants property. A constant can have the following types:

  • (u)intx_t, with x being 8, 16, 32 or 64
  • float, double
  • bool
  • string

constants contains a list of constant definitions, where every item has the following properties:

Required Optional
name  
value cppType

name is the name of the constant. value is the value of the constant. The type of the constant is deduced from its value, but it’s possible to explicitly specify the type that the constant should have in the generated C++ code. E.g. the value 111 will by default be given the type int32_t, but when the cppType is uint8_t, it will get that type. As another example, the value 3.14 will by default be given the type float, but it can also be a string constant when the cppType is string. The latter could alternatively be achieved by prefixing the value with !!str. This forces the YAML parser to treat the value as a string and is unrelated to LRPC.

There is currently no other use case for constants than to provide a single source of truth for constans that are needed on both the client side and the server side. Notably, it is not (yet) possible to reference a constant in other parts of the definition, e.g. as the size of an array. To achieve this kind of behavior, the anchor and alias features of yaml may be used.

Example:

...
# an additional property called my_prop
# is used as an anchor with name array_size
# The additional property is allowed at
# the definition top-level
my_prop: &array_size 55
constants:
  # implicit int32_t
  - name: c0
    value: 111
  # explicit uint16_t
  - name: c1
    value: 111
    cppType: uint16_t
  # explicit string
  - name: c2
    value: 111
    cppType: string
  # using array_size to define a constant and as array size
  - name: c3
    value: *array_size
services:
  - name: srv0
    functions:
      - name: f0
        params:
          - name: my_array
            type: uint16_t
            count: *array_size
...

LrpcType

The LRPC definition file uses LrpcType to describe function arguments, function return values and struct fields.

A LrpcType has the following properties: | Required | Optional | | ——— |——— | | name | count | | type | |

name is the name of the LrpcType.

LrpcType.type

See section data types

LrpcType.count

Specifying count as a number (at least 2) turns the LRPC type into an array of that size. Specifying count as ? turns the LRPC type into an optional. If count is omitted, the type specified by type is used without any modifications