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
  settings
  user_settings

It is not allowed to use additional properties at top level. This can be done under user_settings.

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.

The service ID is encoded in an 8-bit field and ID 255 is reserved for the LotusRPC meta service. This means that a user service can have any ID in the range of 0 to 254.

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
  returns_alias

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.

It is possible to define an alias for the combined function returns with returns_alias. For functions with multiple, complex return values this can significantly improve readability of generated C++ code. The alias must be a valid C++ identifier and not collide with any of the function parameter or return names.

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.

Example 1.

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.

Example 2.

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.

Example 3.

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.

From Python, the value of a constant in the definition can be obtained with the function LrpcDef.constant.

There is currently no other use case for constants than to provide a single source of truth for constants 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 under
# user_settings
user_settings:
  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
...

Settings

LotusRPC allows some level of customization with the following optional properties in the settings section in the definition file.

Property Type/value
rx_buffer_size At least 3
tx_buffer_size At least 3
namespace String
version String
definition_hash_length 0 to 64
embed_definition Boolean
byte_type See byte type table
Byte type Remark
uint8_t Default
int8_t  
char  
char8_t Requires at least C++20. Not enforced by LotusRPC
unsigned char  
signed char  
etl::byte  
std::byte Requires at least C++17. Not enforced by LotusRPC

rx_buffer_size and tx_buffer_size define the receive and transmit buffer size in bytes for generated C++ server code. Default is 256. namespace defines the C++ namespace to generate server code in. By default the code is generated in the global namespace.

version is used to identify the definition with a user-defined version. When specified, it is used by LotusRPC to detect a mismatch between the client and the server (through the version function in the meta service). Another way to detect differences between client and server is to use the definition hash. By default, lrpcg calculates the sha3-256 hash of the definition file when generating code. This results in a 64-character hash string on the server that is used to detect a mismatch between the client and the server (through the version function in the meta service). definition_hash_length is used to truncate the hash string to anything less than 64.

It is possible to embed the LotusRPC definition file in the generated server code. This is enabled by setting embed_definition to true. When not specified, the definition is not embedded. The definition file is compressed by lrpcg and included in the generated code as a constant array of uint8_t.

Using the definition that is embedded on the server from the client side can be done with

  • Using the from_server factory method of the LrpcClient class
  • Specifying definition_from_server as always or once in the lrpcc config file.

byte_type is used to control the type that LotusRPC uses internally for lrpc::bytearray.

User settings

Section user_settings allows for free-format user settings in the LotusRPC definition file. The user can specify any valid YAML data structure under user_settings. The user settings are accessible when visiting a LrpcDef object with a visitor deriving from LrpcVisitor. This flexible approach has no specific use case for LotusRPC and is ignored by LotusRPC internally, but allows users to include custom data in the LotusRPC definition file and use it to their own benefit. User settings may also be useful for creating anchors that can be referenced in other parts of the definition, e.g. as a common array size. See visiting LrpcDef for more information on extending LotusRPC.

Definition overlays

LotusRPC supports merging overlay definitions on top of base definitions. This technique allows for creating variants of a definition without duplicating the entire base structure. For example, you can create platform-specific or variant-specific overlays that selectively add, remove, or replace properties from a base definition.

Overlay files

Overlay files are YAML files like the main definition file and typically also have the .lrpc.yaml extension. An overlay file follows the same structure as the main definition file but only for the slice of the main definition that is modified. Additionally it has a merge_strategy property to control the merge type.

Overlay files can contain multiple YAML documents to specify multiple (conflicting) overlay actions in a single file.

Merge strategy

The merging process is controlled by the merge_strategy property. This property can be specified at any level to control how the definition properties are merged. The merge strategy is inherited from parent properties to child properties.

Strategy Behavior Precondition
add Add a property to base Item does not exist in base1
remove Remove a property from base Item exists in base
replace Replace a property in base Item exists in base

Add, remove and replace overlays on composite properties are always matched by the name property. When adding or removing a basic property from a list, the property is matched by value. It is not possible to replace a basic property in a list directly, but it can be achieved by applying a remove overlay followed by an add overlay.

It is also possible to remove a basic property (e.g. string, bool or int) by assigning null. In this case it is not necessary to provide a merge strategy.

A merge fails when an overlay fails to meet the precondition.

Example 1: Adding a new parameter

Base definition:

name: overlay_example
settings:
  namespace: ov_ex
services:
  - name: MyService
    functions:
      - name: DoWork
        params:
          - name: timeout
            type: uint32_t

Overlay:

services:
  - name: MyService
    functions:
      - name: DoWork
        params:
          - name: retries
            type: uint8_t
        merge_strategy: add

Result: Function DoWork has both timeout and retries parameters.

Example 2: Removing a parameter

Overlay:


services:
  - name: MyService
    functions:
      - name: DoWork
        params:
          - name: timeout
            merge_strategy: remove

Result: Function DoWork no longer has the timeout parameter.

Example 3: Removing a property with null

Overlay:

settings:
  namespace: null

Result: The namespace setting is removed from the settings.

Example 4: Replacing an entire service

Overlay:

services:
  - name: MyService
    functions:
      - name: NewFunction
    merge_strategy: replace

Result: Service MyService is completely replaced; previous functions are removed.

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

  1. When adding a basic item to a list, the item must not exist in the base definition. When adding a composite, named item to a list, the item is added in its entirety when no item with that name exists in the base. When an item with that name does exist in the base, the merge is done recursively on the sub-properties of the composite item