Skip to content

Discussion: A better Message trait #156

@GabrielDertoni

Description

@GabrielDertoni

Introduction

Currently the Message trait is defined like this

pub trait Message
where
    Self: Sized,
{
    fn message_id(&self) -> u32;
    fn message_name(&self) -> &'static str;

    /// Serialize **Message** into byte slice and return count of bytes written
    fn ser(&self, version: MavlinkVersion, bytes: &mut [u8]) -> usize;

    fn parse(
        version: MavlinkVersion,
        msgid: u32,
        payload: &[u8],
    ) -> Result<Self, error::ParserError>;

    fn message_id_from_name(name: &str) -> Result<u32, &'static str>;
    fn default_message_from_id(id: u32) -> Result<Self, &'static str>;
    fn extra_crc(id: u32) -> u8;
}

Then types implementing Message are structs of several *_DATA types that store the actual data for the message. Each of those types has ser and deser methods but no unifying trait to encompass this behaviour. They also have an associated constant ENCODED_LEN and implement Default.

Current limitations and issues

  1. Returning a Result<T, &'static str> isn't very helpfull in this case. There is a single reason for this to fail: the massage with the given name/id doesn't exist! However, the type indicates that many different error messages could be emitted. A better type would be Result<T, MessageDoesNotExist> or simply Option<T>.
  2. message_id_from_name, default_message_from_id, extra_crc, message_id and message_name are all trying to get some metadata about the message, though some do that with an instance of the message and others are static. We could instead return a &'static MessageMeta type that stores all the metadata in a single place. This would allow stuff like getting the message name from an id without having to construct the message. We would also avoid having to do multiple calls if we want multiple pieces of metadata. The metadata struct can also be passed around which is pretty usefull.
  3. In order to get a default instance of a message we need to construct it, even if we only need a reference to it. So, this MessageMeta struct could also store the default value for the message. Then we would have one MessageMeta static for every message *_DATA.
  4. Currently there is a mandatory dependency on heapless, but very little of the library is actually used (only Vec) and those uses could just be standard Rust arrays.
  5. Instead of implementing Bytes and BytesMut, use the bytes crate. It's a tiny crate that does pretty much the same as this crate's implementation, but uses a trait instead of a type, and implements the trait for slices.

A better interface

pub struct MessageMeta<M: Message> {
    pub id: u32,
    pub name: &'static str,
    pub extra_crc: u8,
    pub serialized_len: u8,
    pub default: M,
}

pub trait Message: Sized + 'static {
    fn meta(&self) -> &'static MessageMeta<Self>;
    fn meta_from_id(id: u32) -> Option<&'static MessageMeta<Self>>;
    fn meta_from_name(name: &str) -> Option<&'static MessageMeta<Self>>;

    fn serialize(&self, version: MavlinkVersion, bytes: &mut [u8]) -> usize;

    fn deserialize(
        version: MavlinkVersion,
        msgid: u32,
        payload: &[u8],
    ) -> Result<Self, error::ParserError>;
}

Additionaly, every message could implement another trait

pub trait MessageData<M: Message>: Default + Into<M> {
    fn meta() -> &'static MessageMeta<M>;

    fn serialize_payload(&self, version: MavlinkVersion, payload: &mut [u8]) -> usize;
    fn deserialize_payload(version: MavlinkVersion, payload: &[u8]) -> Result<Self, ParserError>;
}

this trait is parameterized on M: Message since a single *_DATA type can be used in multiple message enums. Issues 4 and 5 are about the internal message implementation and not the external interface, but switching to use them is pretty trivial. For serde, we can use serde_arrays which is also tiny, at least until serde supports const generics.

Also, using bytes might make it easier to integrate with other libraries in the future, such as tokio_util::codec::Decoder.

Final notes

I have implemented pretty much all of these changes already here, but I would like some feedback if these are things you would be interested in changing in the library. It's a big change, but I think the API would be much better with them. Anyway, let me know what you think!

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions