0

I am new to Rust + procedural macro's — apologies because I don't know what I am doing

Background: I am using the rustdds crate to implement DDS. I have created extensive wrapper code to customize these domain participants to talk on custom topics (CustomTopic), using enums. Each arm of the enum is a topic that has an associated DataReader + DataWriter, both of a custom serializable datatype. To do this, I use a HashMap: HashMap<CustomTopic, DataReader<dyn Any>> (one for DataReader, one for DataWriter)

The problem with using dyn Any or any custom trait that implements serializability, is the compiler does not like this because size of the datatype is not known during compilation time.

Work-around: I used enums in the following way:

pub enum CustomDataReader = {
    String(DataReader(String)),
    MyCustomDatatype(DataReader(MyCustomDatatype)), // MyCustomDatatype is serializable
}
// and then
data_readers_hashmap = HashMap<CustomTopic, CustomDataReader>

and this works perfectly. I can now access my data readers / writers using the hash map, AND I can access them using a match:

match datareader {
    CustomDataReader::String => {
        // datareader is of type DataReader<String>
    },
    CustomDataReader::MyCustomDatatype => {
        // datareader is of type DataReader<MyCustomDatatype>
    },
}

Now this fits my application PERFECTLY. The downside, there is a lot of boilerplate (a lot which I have not shown) so I started looking into procedural macros.

Example: Ideally, I would just create a struct, and slap #[derive(CustomDatatype)] above a struct as follows:

#[derive(CustomDatatype)]
struct Car {
    make: String,
    model: String,
    num_wheels: u8,
}

#[derive(CustomDatatype)]
struct Building {
    name: String,
    location: (f32, f32),
}

struct Duck {
    color: String,
}

pub enum CustomDataReader {}
pub enum CustomDataWriter {}

And the macro would add these elements as arms to those two enums:

...

pub enum CustomDataReader {
    Car(DataReader(Car)),
    Building(DataReader(Building)),
}
pub enum CustomDataWriter {
    Car(DataWriter(Car)),
    Building(DataWriter(Building)),
}

Now I can access these data readers / writers using CustomDataReader::Car and CustomDataReader::Building

Failed attempt: I've followed all the steps in making a procedural macro, and I am starting to wrap my head around it. My only issue is, I am unable to actually "append" to an existing enum. I can go ahead and make new enums using quote! just fine

#[proc_macro_derive(CustomDatatype)]
pub fn custom_datatype_derive(input: TokenStream) -> TokenStream {
    let input: DeriveInput = parse_macro_input!(input);
    let struct_name = &input.ident;
    let output = quote! {
        #input

        pub enum CustomDataReader {
            #struct_name(rustdds::no_key::DataReader<#struct_name>),
        }
        pub enum CustomDataWriter {
            #struct_name(rustdds::no_key::DataWriter<#struct_name>),
        }
    }
    output.into()
}

But the problem with this, is it generates code likes this:

// first
pub enum CustomDataReader {
    Car(DataReader(Car)),
}
pub enum CustomDataWriter {
    Car(DataWriter(Car)),
}

// then
pub enum CustomDataReader {
    Building(DataReader(Building)),
}
pub enum CustomDataWriter {
    Building(DataWriter(Building)),
}

Which is simply not the functionality I want. And I have been stuck on this for the past couple of weeks and have looked into other macro types. Not sure where to go from here. Any help or general advice is appreciated

EDIT: Response to comment recommended using generics: I have a trait that implements the serde::Serialize and serde::Deseralize and used it to create the exact struct you're talking about:

pub trait DDS_Msg: Serialize + for<'a> Deserialize<'a> + fmt::Display + Clone {}

struct CustomDatatype {};
impl DDS_Msg for CustomDatatype {}

pub struct Messenger {
    pub reader: Box<DataReader<dyn DDS_Msg>>,
    pub writer: Box<DataWriter<dyn DDS_Msg>>,
}

But then I get the following stack trace:

error[E0277]: the size for values of type `(dyn ddstypes::DDS_Msg + 'static)` cannot be known at compilation time
   --> src/utils/ddstypes.rs:123:17
    |
123 |     pub reader: Box<DataReader<dyn DDS_Msg>>,
    |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
    |
    = help: the trait `Sized` is not implemented for `(dyn ddstypes::DDS_Msg + 'static)`
note: required by a bound in `rustdds::no_key::DataReader`
   --> /home/user/.cargo/registry/src/github.com/rustdds-0.7.11/src/dds/no_key/datareader.rs:49:23
    |
49  | pub struct DataReader<D: DeserializeOwned, DA: DeserializerAdapter<D> = CDRDeserializerAdapter<D>> {
    |                       ^ required by this bound in `rustdds::no_key::DataReader`

So since I need to predefine my datatypes during compilation time, a statically typed enum does the trick

  • 1
    Separate proc-macro invoacations cannot share data between them (as far as I'm aware at least) so I don't think this particular design can work. But at a glance, this looks like generics are a better fit anyways. Instead of `CustomData{Reader, Writer}` being enums maybe they should be traits that consumers can be generic over? – isaactfa Jun 14 '23 at 23:26
  • @isaactfa I tried to put my full comment here, but I just made an edit to my post above to show that I did try that but could not get it to work, due to the statically known size of deserializable structs when using `DataReader` + `DataWriter` – Arpad Voros Jun 15 '23 at 00:16
  • For the generics-based code, can you post a complete reproducing example? (Preferably something that can be tried on the [Playground](https://play.rust-lang.org)) And it is possible to do this with proc macros, but you'll have to put all the `CustomDatatype` into a single macro invocation, e.g. `custom_data_type!{ struct Car {...} struct Building {...} }`. Though that will break `cargo fmt` and rust-analyzer-based editors, so I'm not sure I would recommend it. – Caesar Jun 15 '23 at 04:54
  • As isaactfa said, derive macros won't work because they can't change those enums. Though it is far from ideal, you could use a function-like macro instead and pass in everything (the car struct, the building struct and two two enums) as an argument. You can then 'recreate' the structs and enums with their new 'arms'. (But obviously if generics work, that's even better.) – Hieron Jul 14 '23 at 17:49

0 Answers0