Question about Rust auto-generation of static struct instances

I am working on simplifying the flow of specifying low level return codes in Rust by using procedural macros.

This is some of the library code

/// Simple [u16] based result code type which also allows to group related resultcodes.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ResultU16 {
    group_id: u8,
    unique_id: u8,
}

use serde::{Deserialize, Serialize};
use serde_hex::{SerHex, StrictCapPfx};
#[derive(Debug, Copy, Clone, Serialize)]
pub struct ResultU16Info {
    pub name: &'static str,
    pub result: &'static ResultU16,
    pub group_str: &'static str,
    pub info: &'static str,
}

Right now, the code, as written by the user, looks something like this:

#[resultcode(info = "This is a test result where the first parameter is foo")]
const TEST_RESULT: ResultU16 = ResultU16::new(0, 1);
// Create named reference of auto-generated struct, which can be used by IDEs etc.
const TEST_RESULT_EXT_REF: &ResultU16Info = &TEST_RESULT_EXT;

I basically implemented a basic form of introspecition which later allows to generate something like CSVs with all these returncodes. The *_EXT structure is generated automatically by the resultcode attribute macro. Not sure if that is the cleanest/most rusty approach here, but this has worked for me.

When using and scaling this mechanism, I usually keep a static slice of ResultU16Infos
for easier usage in my CSV generators, for example something like this

    pub const HK_ERR_RESULTS: &[ResultU16Info] = &[
        TARGET_ID_MISSING_EXT,
        UNKNOWN_TARGET_ID_EXT,
        UNKNOWN_TARGET_ID_EXT,
        COLLECTION_INTERVAL_MISSING_EXT,
    ];

I now always have to update this list as well. It might seem a minor inconvenience, but it's quite annoying and it is also easy to forget updating these lists when adding new returncodes.

What would be some possible ways to automatically add auto-generated structures to some list in some shape or form?

Kind Regards
RM

You can use a macro which consumes the declarations and produces both the same declarations and a list. Here is a self-contained example:

use paste::paste;

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct ResultU16 {
    group_id: u8,
    unique_id: u8,
}

#[derive(Debug, Copy, Clone)]
pub struct ResultU16Info {
    pub name: &'static str,
    pub result: &'static ResultU16,
    pub group_str: &'static str,
    pub info: &'static str,
}

macro_rules! result_codes {
    (
        $list_name:ident :
        $(
            #[resultcode(info = $info:literal)]
            const $rc:ident: ResultU16 = $init:expr;
        )*
    ) => {
        paste! {
            $(
                const $rc: ResultU16 = $init;
                const [< $rc _EXT >] : ResultU16Info = ResultU16Info {
                    name: stringify!($rc),
                    result: &$rc,
                    group_str: "",
                    info: $info,
                };
            )*
            const $list_name: &[ResultU16Info] = &[
                $( [< $rc _EXT >] ),*
            ];
        }
    };
}

result_codes! {
    HK_ERR_RESULTS:
    #[resultcode(info = "This is a test result where the first parameter is foo")]
    const TARGET_ID_MISSING: ResultU16 = ResultU16 { group_id: 1, unique_id: 2 };
}

fn main() {
    dbg!(HK_ERR_RESULTS);
}

In this example, I've made the macro interpret the resultcode attribute as well as generating the list, but presumably for your real application, that would be done by whatever library you're using instead and the macro should pass on the attribute instead. In that case, you'd match arbitrary attributes with:

$( #[$attr:meta] )*

Hi,

thanks for the suggestion, excellent idea with the macro!

If you know that all the result codes are going to be defined within your library, an alternative to macros is code generation.

In my WIT Language Server, I have a cargo xtask codegen diagnostics helper command that will read my source code to find all the types that can be turned into diagnostics, and from there it will generate a fn all_diagnostics() -> Vec<Diagnostic info<'static>> function which can be used to get information about all the diagnostics.

The generated code:

To make sure things never get out of date, this gets run automatically by cargo test.

From there, I have another test which will automatically save the Vec<DiagnosticInfo>s to a JSON file so I can generate a pretty diagnostics index HTML page (via cargo xtask doc).

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.