Note: All C++ code shown here is generated by lrpcg. You do not write it — you implement the abstract methods it declares.

Overview

lrpcg cpp generates a set of header files for each interface definition. For a definition named example with services math and sensor, the output looks like (some files omitted for clarity):

treeView-beta
"generated"
  "example"
    "lrpccore"
    "example.hpp"
    "math_shim.hpp"
    "sensor_shim.hpp"
    "..."

The top-level include example.hpp pulls in all of the other files and is the recommended way to include code generated by LotusRPC in your project. The *_shim.hpp files contain the abstract base classes for the services.

Server class

The top-level include exposes a type alias for the fully configured server:

// example.hpp (generated)
namespace ex
{
    using example = lrpc::Server<...>;
}

lrpc::Server is a template class parameterized with buffer sizes and the meta service. You interact with it through the following interface:

lrpcTransmit (pure virtual)

virtual void lrpcTransmit(lrpc::span<const uint8_t> bytes) = 0;

You must subclass the generated server and implement this method. LotusRPC calls it whenever it has a frame ready to send to the client. Wire it to your hardware transmit routine (UART, SPI, TCP socket, etc.).

class MyServer : public ex::example
{
    void lrpcTransmit(lrpc::span<const uint8_t> bytes) override
    {
        uart_write(bytes.data(), bytes.size());
    }
};

registerService

void registerService(Service& service);

Registers a service instance with the server. Call once per service before processing any data. The service object must outlive the server.

lrpcReceive

void lrpcReceive(uint8_t byte);                    // single byte
void lrpcReceive(lrpc::span<const uint8_t> bytes); // span of bytes (lvalue container implicitly converted to span)

template <typename TContainer>
void lrpcReceive(const TContainer &bytes);         // container of bytes

Feeds incoming bytes to the server. Call from your receive interrupt or polling loop. LotusRPC handles framing internally — pass bytes as they arrive.

Service class

For each service in the definition, lrpcg generates a shim class. You derive from it and implement its pure virtual methods. For a service named math with one function add(int32_t a, int32_t b) -> int32_t:

// math_shim.hpp (generated)
namespace ex
{
    class math_shim : public lrpc::Service
    {
    protected:
        virtual int32_t add(int32_t a, int32_t b) = 0;
    };
}

Your implementation:

class MathService : public ex::math_shim
{
protected:
    int32_t add(int32_t a, int32_t b) override
    {
        return a + b;
    }
};

Single return value

functions:
  - name: get_temperature
    returns:
      - { name: temp, type: float }

Generated shim method:

virtual float get_temperature() = 0;

Implementation:

float get_temperature() override
{
    return read_sensor();
}

Multiple return values

functions:
  - name: get_status
    returns_alias: Status
    returns:
      - { name: code, type: uint8_t }
      - { name: message, type: string }

Generated shim method:

using Status = std::tuple<uint8_t, lrpc::string_view>;
virtual Status get_status() = 0;

Implementation:

Status get_status() override
{
    return {0, "OK"};
}

Client stream

The client sends data to the server. The server receives it through a pure virtual method and can optionally request the client to stop.

streams:
  - name: log_data
    origin: client
    finite: true
    params:
      - { name: entry, type: string }

Generated shim methods:

// Called for each incoming message. `final` is true for the last message (finite streams only).
virtual void log_data(lrpc::string_view entry, bool final) = 0;

// Call this to ask the client to stop sending (optional).
void log_data_requestStop();

Implementation:

void log_data(lrpc::string_view entry, bool final) override
{
    store_log(entry);
    if (final) { flush_log(); }
}

Server stream

The client initiates and terminates the stream. The server sends data back by calling a non-virtual response method.

streams:
  - name: sensor_data
    origin: server
    params:
      - { name: value, type: uint16_t }

Generated shim methods:

// Called when the client sends 'start'.
virtual void sensor_data() = 0;

// Called when the client sends 'stop'.
virtual void sensor_data_stop() = 0;

// Call this to send a data message to the client.
void sensor_data_response(uint16_t value);

Implementation:

void sensor_data() override
{
    streaming_ = true;
}

void sensor_data_stop() override
{
    streaming_ = false;
}

// Somewhere in your application loop:
void on_new_measurement(uint16_t v)
{
    if (streaming_) { sensor_data_response(v); }
}

Finite server stream

For a finite server stream, the response method has an additional bool final parameter:

void sensor_data_response(uint16_t value, bool final);

Pass final = true with the last message to signal end of stream.

Type mapping

The table below shows how LotusRPC definition types map to C++ types in function parameters and return values.

LotusRPC type C++ type
(u)intx_t (u)intx_t
bool, float, double bool, float, double
enum enum class
struct C++ struct
string (fixed or auto size) lrpc::string_view
byte array lrpc::bytearray
array (count N) lrpc::span<const T>
optional lrpc::optional<T>
multiple returns std::tuple<T1, T2, ...>

Type aliases

Alias Underlying type Notes
lrpc::byte uint8_t (default) Configurable via byte_type setting
lrpc::bytearray etl::span<const lrpc::byte> View over a byte buffer
lrpc::string_view etl::string_view View over a string buffer
lrpc::span<T> etl::span<T> Generic span
lrpc::array<T, N> std::array<T, N> Fixed-size array
lrpc::optional<T> etl::optional<T> Optional value

Ownership and lifetimes

In LotusRPC, incoming bytes are decoded from the server receive buffer and forwarded to your function implementation as arguments. Return values are encoded back into the server transmit buffer. LotusRPC uses value semantics as much as possible, with a few exceptions for efficiency.

Note: Returning a reference of any kind to a local variable leads to undefined behavior in C++. The ownership rules below follow the same principle.

Type Parameter semantics Return semantics
(u)intx_t Passed by value Returned by value
bool, float, double Passed by value Returned by value
enum Passed by value Returned by value
Struct Decoded into a local copy, passed by const& Returned by value
String (fixed or auto size) lrpc::string_view into the receive buffer lrpc::string_view — caller must ensure the viewed string outlives the function
Array Decoded into a local copy (for alignment), passed as lrpc::span lrpc::span — same lifetime rules as string
Bytearray lrpc::bytearray (span) into the receive buffer lrpc::bytearray — same lifetime rules as string

For strings, arrays and bytearrays: the parameter can be used safely inside the function, but must be copied if needed beyond the call. Struct fields that are strings or bytearrays follow the same rules as standalone strings/bytearrays.

Examples

For a full end-to-end walkthrough — definition, code generation, service implementation, and Python client — see the Math service example. For an example on real embedded hardware with HAL-based transport, see the STM32 example on real hardware.