LibCyphal Reference Implementation

repository·main·Indexed 43 days ago

https://github.com/opencyphal-garage/libcyphal

A portable C++14 reference implementation of the Cyphal protocol stack designed for high-integrity aerospace and robotic applications. It provides a header-only wrapper over low-level 'lizard' libraries (like LibCANard and LibUDPard) and is suitable for embedded systems, Linux, and POSIX-compliant RTOSs. The library features a three-layer architecture (transport, presentation, and application), minimal heap usage via PMR, and an exception-free design.

Tokens
11.2K
Snippets
26
Records
37
Agent score
56%

What's inside LibCyphal

  1. Overview of LibCyphal architecture

    main

    LibCyphal is a high-level, header-only C++14 wrapper designed to sit atop existing Cyphal implementation libraries (collectively called "lizards," such as LibCANard and LibUDPard). It is designed for high-integrity software development and is suitable for integration into real-time applications ranging from baremetal MCUs to POSIX operating systems (e.g., GNU/Linux).

    Target Hardware Note: LibCyphal is NOT intended for targets with extremely strict memory constraints (e.g., low-end MCUs with less than ~100k RAM). For such devices, it is recommended to use the underlying "lizard" libraries directly.

  2. How the Presentation layer works in LibCyphal

    main

    The Presentation layer is the primary interface for interacting with DSDL-generated types. It uses a non-polymorphic, non-generic controller class called Presentation to manage port objects.

    Key Concepts:

    • Port Objects: The layer provides four types of ports: Publishers, Subscribers, RPC Clients, and RPC Servers.
    • Lifecycle & Resource Management:
      • Port objects (publishers/subscribers/clients) are returned by value.
      • Multiple ports on the same port-ID share a single, hidden, heap-allocated implementation object. This object is destroyed only when the last port using that port-ID is destroyed.
      • To avoid runtime costs, de-initialization of transport sessions is postponed until the next IRunnable::run cycle.
    • Polymorphism: Unlike many C++ libraries that rely heavily on template metaprogramming, LibCyphal prefers runtime polymorphism for DSDL types. Generated types implement the IComposite interface to support runtime type identification and serialization/deserialization.
    class IComposite : public cetl::IPolymorphicType
    {
    public:
        [[nodiscard]] virtual SerializeResult _serialize_(cetl::bitspan out_buffer) const = 0;
        [[nodiscard]] virtual SerializeResult _deserialize_(cetl::const_bitspan in_buffer) = 0;
        [[nodiscard]] virtual std::string_view _getFullName_() const noexcept = 0;
        [[nodiscard]] virtual std::string_view _getFullNameAndVersion_() const noexcept = 0;
    
    protected:
        IComposite() = default;
        virtual ~IComposite() noexcept = default;
    };
  3. Use PMR-aware CETL containers

    main

    To maintain the no-heap policy, LibCyphal utilizes several PMR-aware (Polymorphic Memory Resource) components from the CETL library:

    • cetl::any: A PMR-aware reimplementation of std::any that uses the RTTI substitute instead of standard RTTI. It supports moving to handle non-copyable objects like memory buffers.
    • cetl::function: A heapless implementation that allocates callables within a private arena or a PMR-provided buffer.
    • cetl::shared_ptr: A PMR-enabled shared pointer where the reference counter is allocated using a provided std::pmr::memory_resource rather than the global heap.
    • ImplementationCell: A pattern used for type erasure without pointers, leveraging cetl::any to store objects in an internal arena rather than on the heap.
  4. Error handling patterns in LibCyphal

    main

    LibCyphal avoids exceptions and instead uses algebraic types for error handling.

    Key patterns include:

    • Using types similar to std::expected (provided by CETL).
    • Using std::variant (or CETL alternatives) to hold error states, where each state can contain specific error information (like errno).
    • Using the AppendType helper to extend error variants as they propagate up the call stack.
    namespace detail
    {
    template <typename T, typename... Args>
    struct AppendType;
    template <typename... A, typename... B>
    struct AppendType<std::variant<A...>, B...>
    {
        using Result = std::variant<A..., B...>;
    };
    }
    
    /// Usage:
    ///     using NewVariant = AppendType<OldVariant, NewAlternative1, NewAlternative2>
    template <typename T, typename... Args>
    using AppendType = typename detail::AppendType<T, Args...>::Result;
  5. Use ScatteredBuffer for payload management

    main

    Because Cyphal lizards (low-level drivers) may return fragmented memory, LibCyphal uses ScatteredBuffer to provide a uniform, linearized view of the payload.

    Key features:

    • RAII Semantics: The buffer is movable but not copyable. When the ScatteredBuffer instance is destroyed, it automatically handles the memory deallocation in a lizard-specific manner.
    • Non-copying API: Use the .copy() method to move data from the buffer to your own destination to avoid expensive memory copies.
    • Size: Use .size() to get the total number of bytes stored.
    // Example of copying data out of a ScatteredBuffer
    void process_payload(const ScatteredBuffer& buffer) {
        std::array<std::byte, 128> destination;
        std::size_t bytes_copied = buffer.copy(0, destination.data(), destination.size());
        // Use bytes_copied to know how much valid data was retrieved
    }
  6. Use the Registry to manage named registers

    main

    The Registry (implementing IRegistry and IIntrospectableRegistry) allows you to manage a collection of named registers. Registers are accessed by name (using a 64-bit hash for efficiency) and provide a way to get or set values.

    Core Interfaces

    • IRegistry: Provides get(name) and set(name, value) methods. get returns a ValueWithMetadata (containing the value and RegisterFlags), and set returns an optional SetError.
    • IIntrospectableRegistry: Extends IRegistry with index(index) and size() to allow iterating over the register set.

    Register Flags

    • mutable_: Indicates if the register can be changed via set().
    • persistent: Indicates if the register value should be retained across application restarts.

    SetError Codes

    • Existence: Register does not exist.
    • Mutability: Register is immutable.
    • Coercion: Value cannot be coerced to the register type.
    • Semantics: Rejected by register semantics (e.g., out of range).
    // Example of the basic interface
    std::optional<ValueWithMetadata> val = registry.get("my_register");
    if (val) {
        // use val->value
    }
    
    std::optional<SetError> err = registry.set("my_register", dsdl_val);
    if (!err) {
        // success
    }
  7. How LibCyphal handles type identification without RTTI

    main

    To avoid dependency on opaque compiler-generated RTTI code and to support environments where RTTI is disabled, LibCyphal uses a substitute mechanism for type identity.

    Important Limitation: The primary substitute relies on the address of a static local variable in an inline function. While this is guaranteed to be unique within a single executable, dynamic linking (shared objects) can break this, as different shared objects may have their own instantiations of the type ID. If the library is used across dynamically linked boundaries, RTTI support (LIBCYPHAL_USE_RTTI) should be enabled to ensure correctness.

    /// A simplified substitute for RTTI that allows querying the identity of types.
    /// Returns an object of an unspecified type for which operator== and operator!= are defined.
    /// Objects for the same type compare equal. Ordering is not defined.
    template <typename>
    auto getTypeID() noexcept
    {
        static const struct {} placeholder{};
        return static_cast<const void*>(&placeholder);
    }
    using TypeID = decltype(getTypeID<void>());
  8. Understand the LibCyphal Transport Layer Model

    main

    The transport layer in LibCyphal is composed of ITransport instances that manage data flows through various types of sessions. Sessions are categorized by their purpose:

    • Message Sessions: Parameterized by subject_id. Includes IMessageRxSession (receiving) and IMessageTxSession (sending).
    • Request/Response Sessions: Parameterized by service_id.
      • IRequestRxSession and IResponseTxSession are used by RPC servers and are agnostic of the remote node ID.
      • IResponseRxSession and IRequestTxSession are used by RPC clients and are parameterized by both service_id and remote_node_id.

    All transport operations are non-blocking. Background tasks (like enqueuing data) are executed via the IRunnable interface through periodic servicing of the run() method.

  9. Understand the LibCyphal Application Layer

    main

    The application layer is the highest level of the LibCyphal stack and is the only part that depends on DSDL-generated types. It is built on top of the presentation layer and provides high-level abstractions for Cyphal nodes.

    Key components include:

    • Node class: A central entity that wraps core Cyphal node components, such as a heartbeat publisher, a uavcan.node.GetInfo RPC-service implementation, and a registry.
    • Registry: A system for managing named registers (getters/setters) that can be stored in-memory or backed by non-volatile storage.
    • Standard Implementations: A collection of common Cyphal specification requirements provided as ready-to-use modules.
  10. How the LibCyphal layers work together

    main

    LibCyphal is organized into three distinct functional layers that build upon one another:

    1. libcyphal::transport: The lowest layer. It abstracts the underlying "lizards" via the ITransport interface. At this level, data is handled as serialized binary blobs. It provides the glue logic between the transport protocols and the platform layer.
    2. libcyphal::presentation: The middle layer. It sits on top of the transport layer and provides:
      • DSDL (de)serialization (using Nunavut-generated code) so users can work with DSDL objects instead of raw blobs.
      • Demultiplexing of incoming transfers, allowing multiple subscribers to a single subject.
      • An interface for invoking and serving RPC service calls.
    3. libcyphal::application: The top layer. It provides a node-centric API and implements standard protocol functions like the register interface, heartbeat publication, and port usage messages. This layer depends on Nunavut-generated code from standard DSDL definitions.
  11. Design principles of LibCyphal

    main

    LibCyphal is designed for high-integrity, real-time embedded systems with the following core principles:

    • Exception-aware, but no exceptions: The library does not use C++ exceptions itself but is designed to be compatible with applications that do.
    • Minimal Heap Usage: Most allocations occur once during initialization using Polymorphic Memory Resources (PMR). The library avoids the global heap.
    • No-allocation policy exceptions: The only runtime allocations typically performed are by transport-layer queues and memory reassembly buffers (managed by 'lizards').
    • Type Erasure: Used to reduce coupling and uphold encapsulation, often utilizing small-object-optimized containers to avoid heap allocation.
    • RTTI-independent: Does not require C++ Runtime Type Identification (RTTI); instead, it uses substitutes provided by the CETL library to identify types.
  12. Implement or use IRunnable for execution flow

    main

    LibCyphal uses an execution model based on the IRunnable interface. To perform asynchronous operations (like servicing a transport), you must periodically call run(now: TimePoint).

    For managing multiple runnable entities in a single thread, use AggregateRunnable. This class implements IRunnable itself and allows you to register/unregister multiple inferior runnables.

    Note on I/O Multiplexing: The library supports two execution strategies:

    1. Stateless (IPollSet): Reconstructs the poll set from scratch every cycle. Good for simple poll/select loops.
    2. Stateful (IMultiplexer): Maintains a persistent set of pollable entities, allowing efficient use of epoll or kqueue. This uses RAII Handle objects to manage pollable lifetimes.
    template <std::size_t Capacity>
    class AggregateRunnable final : public IRunnable
    {
    public;
        /// Invokes run() on all members in an unspecified order.
        void run(const TimePoint now, IPollSet& poll_set) override;
        /// Returns true on success, false if the capacity is exhausted.
        [[nodiscard]] bool add(IRunnable& inferior);
        /// Returns true if such inferior was found and removed, false if not found.
        [[nodiscard]] bool remove(IRunnable& inferior);
    };