I'm fairly new to Rust and have started a communications project for a drone flight system on an embedded flight computer. I am mainly having issues with how to best structure my "communications service" that handles different component messages. My problem mainly stems from the fact that different messages, depending on the component it is coming from and going to, has different headers with varying information and I am unsure the best way to construct the protocol handler to build these different message types. Each message does have a primary header that is the same for all that consists of information like:
But after this generic data, each message type has different appending information to the header. Here is a simplified construction of the message types below:
Send_Command:
address -> component address (destination): u32
command_id -> to identify type of command: u16
confirm -> state whether a confirmation reply is needed: u8
Ack:
AckId -> for logging and identifying ack process: u8
Payload -> ack message: u8
Request_data:
address -> component address (destination): u32
command_id -> to identify type of command: u16
confirm -> state whether a confirmation reply is needed: u8
send_rate -> maximum rate allowed for delivery of data: u16
payload -> all data as requested (varying length): u8 vector
I'm coming from a background in C++ where I could handle this problem with classes and subclass extensions but I am not certain of the best way to structure this in Rust. Is the best way to create an enum that holds all message types, a struct with the primary header information and then have a "build" functions that returns the primary header and a vector with the other information based on the message type respectively?
I just feel like there could be a simpler way or more elegant Rusty way. Perhaps I am missing a way to do this with traits or additional enums and types. I just want to try and avoid having to create 10 different functions to handle the 10 different message types if possible.
When you need some sort of polymorphism in Rust, you usually have two choices.
The first choice is, as you observed, creating an enum. This makes sense when the set of choices is known in advance and you do not need to extend it, nor do you want to let downstream users of the code to be able to add new cases.
Semantically, it is also my perception that enums are appropriate when the use of the data type is limited to a very specific, "singular" (so to say) purpose, there is a clear sense of "either this or that" kind of choice, and the enum cases are thus strongly related. Great examples of this notion are Option and Result from the standard library. Using them basically says that "Whatever you do, this is conceptually the same kind of value you get back, but it's either present or not" in the case of Option or "but it either represents a successful or a failed operation" for Result.
Another choice would be the use of traits, either as generics (static dispatch and compile-time monomorphization) or as trait objects (dynamic dispatch). Generics and trait objects are typically used when the set of all possible types is not known at compile time, or when you want your code to be extensible by 3rd-party code, by means of implementing the traits you provide.
In contrast with an enum, creating several different types and making them implementing the same trait suggests to me that there is no specific relationship between these types, except that from some point of view, they can be described similarly. A good example from the standard library is Display: it's not possible to enumerate all the types that will ever be printable, nor would it be desirable, because in general they have nothing in common, except the one specific aspect of being printable when one so desires.
As I have explained in a previous post, I typically prefer to wrap HTTP APIs using separate request types implementing a common trait, since they are not that related. They are just uniformly-described data structures, of which the use will result in completely different actions on the part of the API.
Therefore, I'd be inclined to say that for a communications protocol, it would also be appropriate to go with a generics-based approach instead of an enum, although it's kind of hard to tell for sure without knowing more details about the code, the higher-level design you had in mind, and the precise purpose of the protocol in question.