Rho Rho language and toolchain

Protocol schemas

A schema is a rho file that describes what crosses a link — between a node and
an edge, between an edge and the cloud — rather than what a program does. It is
written once and generated for both ends, so the two can only disagree if the
schema disagrees with itself.

The output of a schema is not a translation of a program. It is an interface:
fixed bytes for the node, typed values and a handler for the service. It is
generated by its own command, apart from a project's build, because the two ends
are built by different toolchains on different machines.

Declarations

module telemetry
namespace telemetry.v1

enum Severity {
    INFO = 0
    WARN = 1
    ERROR = 2
}

message MotorTelemetry {
    timestamp_ms: u32
    current_amps: f32
    rpm: f32
    status: Severity
}

rpc SetMotorSpeed {
    request {
        motor_id: u8
        target_rpm: f32
    }
    response {
        success: bool
        actual_rpm: u16
    }
}
  • namespace a.b.vN; names the schema and its version. The version is the
    number in the last component, or 1, and it is written into every message.
  • enum values are written, not counted. A wire protocol numbers its values,
    and a reader can only trust a number it can see.
  • message is an ordered set of fields. It is a struct that makes a promise
    a struct does not: a byte layout and a version.
  • rpc is a request and a response, each a set of fields.

message, rpc and namespace are contextual: they name a declaration only
where one begins, so a field or a variable may still be called one.

The wire

A message is its version byte and its fields in declaration order, each at a
fixed width. Scalars are little-endian; an enum is a 32-bit value; a string[N]
is N bytes and a bare string is 64; a nested message is its own version byte
and fields; an array is its elements end to end.

Little-endian is not the host's byte order but the schema's, so a big-endian node
and a little-endian one read each other's messages. Reading and writing a message
allocates nothing, on either side.

A message that contains itself cannot be packed and is reported where it is
found.

Generating

# C for the node: protocol.h/.c (the bytes) and protocol_bus.h/.c (transport)
rho protocol generate --target c --out build/protocol/c .

# Go for the edge: protocol.go
rho protocol generate --target go --out build/protocol/go --package telemetry .

# Rho for an edge written in Rho: protocol.rho, included from the program
rho protocol generate --target rho --out src/gateway schemas/telemetry.rho

Every .rho under the given paths is read into one schema, so messages may live
in one file and rpcs in another.

The C side

protocol.h carries a typedef enum per enum, a __attribute__((packed))
struct per message, a _WIRE_SIZE macro, and zero-allocation
Name_serialize/Name_deserialize. protocol_bus.h carries, per rpc, a
publish_request/publish_response and a subscribe/unsubscribe over the
runtime's event bus (robohub_core_event_publish and friends), so a node's
handler is reached by an event rather than a call.

The Go side

protocol.go carries a struct per message with json tags, typed constants per
enum value, and MarshalBinary/UnmarshalBinary that write the same little-endian
bytes the C side reads. Each rpc gets a RegisterName(mux, path, handler) that
puts the handler on an http.ServeMux, JSON on the outside and the wire beneath.

The Rho side

protocol.rho is a Rho module a program includes. Each message becomes a
struct, its _marshal/_unmarshal written field by field with the same
little-endian order, and _valid to reject a payload that is short or of another
schema version. An enum becomes u32 constants, because the wire is a 32-bit
value and Rho has no cast from an enum to a number.

It also carries the transport — _frame and _unframe, with COBS and the
Fletcher-16 check — so a Rho edge speaks the same framing a node's
robohub-transport library does, from one description rather than from a second
implementation that must be kept in step. A TransportFrame returns ok, the
header and the payload, and the program decides what each message id means.