jcaesar / structstruck

Rust nested structs
MIT License
53 stars 3 forks source link

Inner `union` Support #5

Closed halotroop2288 closed 1 year ago

halotroop2288 commented 1 year ago

My goal is to port some particularly awful C++ code for legacy support (emulation), and it makes heavy use of patterns like:

struct x_thing {
    union {
        u32_t value;
        struct {
            type_a thing_a : 4;
            type_b thing_b : 28;
        };
    };
    char some_data[0xFF];
};

It would be nice if you could support this pattern to maybe save me some time. 😅

I'm hoping for an implementation something like this:

structstruck::strike! {
    struct x_thing {
        a: union {
            value: u32,
            b: struct {
                thing_a: TypeA,
                thing_b: TypeB,
            },
        },
        some_data: [char; 123],
    }
}
jcaesar commented 1 year ago

Done. Could you take it for a spin before I publish the release?

halotroop2288 commented 1 year ago

Done. Could you take it for a spin before I publish the release?

Certainly! I've done a bit of testing, it seems that Rust doesn't like it when you do this:

strike! {
    union XEX2Version {
        value: u32be,
        fields: struct {
            major: u32be,
            minor: u32be,
            build: u32be,
            qfe: u32be,
        },
    }
}

Or this:

pub enum XEX2SectionType {
    XEXSectionCode = 1,
    XEXSectionData = 2,
    XEXSectionReadOnlyData = 3,
}

strike! {
    struct XEX2PageDescriptor {
        a: union {
            value: u32be,
            b: struct {
                info: XEX2SectionType,
                page_count: u32be,
            },
        },
        data_digest: [u8; 0x14],
    }
}

Here's the error:

error[E0740]: unions cannot contain fields that may need dropping
...
note: a type is guaranteed not to need dropping when it implements `Copy`, or when it is the special `ManuallyDrop<_>` type
help: when the type does not implement `Copy`, wrap it inside a `ManuallyDrop<_>` and ensure it is manually dropped

This is also a problem for some structs that aren't anonymously nested like this. It's kind of annoying.

Also, if there's no name for the anon struct, it does this:

error: proc macro panicked
...
help: message: cannot parse named fields: expected `:` punct, got Some(Group { delimiter: Brace, stream: TokenStream [Ident { ident: "major", span: #0 bytes(18977..18982) }, Punct { ch: ':', spacing: Alone, span: #0 bytes(18982..18983) }, Ident { ident: "u32be", span: #0 bytes(18984..18989) }, Punct { ch: ',', spacing: Alone, span: #0 bytes(18989..18990) }, Ident { ident: "minor", span: #0 bytes(19003..19008) }, Punct { ch: ':', spacing: Alone, span: #0 bytes(19008..19009) }, Ident { ident: "u32be", span: #0 bytes(19010..19015) }, Punct { ch: ',', spacing: Alone, span: #0 bytes(19015..19016) }, Ident { ident: "build", span: #0 bytes(19029..19034) }, Punct { ch: ':', spacing: Alone, span: #0 bytes(19034..19035) }, Ident { ident: "u32be", span: #0 bytes(19036..19041) }, Punct { ch: ',', spacing: Alone, span: #0 bytes(19041..19042) }, Ident { ident: "qfe", span: #0 bytes(19055..19058) }, Punct { ch: ':', spacing: Alone, span: #0 bytes(19058..19059) }, Ident { ident: "u32be", span: #0 bytes(19060..19065) }, Punct { ch: ',', spacing: Alone, span: #0 bytes(19065..19066) }], span: #0 bytes(18963..19076) })

But you'd probably expect that in Rust.

halotroop2288 commented 1 year ago

I'm kind of considering finding a workaround so I don't have to use unions at all. unions are of course "intrinsically unsafe" so it's probably the best course of action. Still, I think it would be cool to have that working in this crate. :)

jcaesar commented 1 year ago

it it seem that Rust doesn't like it when you do this:

Yeah, but that has little to do with structstruck. If you try cargo expand and use the source from there, the same error still appears. (The only thing that's different is that the diagnostic isn't in the wrong place. Not sure what to do about that.)

struct B {
    info: XEX2SectionType,
    page_count: u32be,
}
union A {
    value: u32be,
    b: B,
}
struct XEX2PageDescriptor {
    a: A,
    data_digest: [u8; 0x14],
}

You can get around that by just doing what rustc says. Your stuff needs to be Copy, which rust takes as a proof that it is "plain old data" (or whatever the rust term for that is).

#[derive(Clone, Copy)]
pub enum XEX2SectionType {
    XEXSectionCode = 1,
    XEXSectionData = 2,
    XEXSectionReadOnlyData = 3,
}

structstruck::strike! {
    struct XEX2PageDescriptor {
        a: union {
            value: u32be,
            b: struct {
                #![strikethrough[derive(Clone, Copy)]]
                info: XEX2SectionType,
                page_count: u32be,
            },
        },
        data_digest: [u8; 0x14],
    }
}

I did however find one bug: If you take the other part of rustc's advice and do b: std::mem::DropManually<struct { … }>, venial chokes on the :. Again unrelated to this, I'll fix it some other day.

Also, if there's no name for the anon struct, it does this:

You mean like this?

union XEX2Version {
    value: u32be,
    struct {
        major: u32be,
    }
}

This is partially venial not really being on board with the whole "a compiler is an error reporting tool with a code generation side gig", and partially a disadvantage of something like structstruck: The grammar allowed by structstruck is just much more complex, so the error messages won't be as helpful.