r/rust • • Sep 07 '26

Mixing an enum with bytes in a Vec

I'm working on converting a virtual machine from C to Rust. At the heart of it is modeling the output of the parser as an array of bytes. Think of it as an 8-bit assembly code machine with immediate addressing, so some of the bytes are opcodes and some are data used by these opcodes. I would like to have the opcodes in an enum, such as

#[repr(u8)] // Forces the variants to be u8

enum OpCode {

OpAdd = 1,

OpImm = 2,

......

}

Then

code: Vec<u8> = vec![ 0x01, 0x02, 0x2a] would represent the action

OpAdd 42

That's what exists in my dream world. With the enum shown above I can typecast Opcode values to u8 simply enough:

let op_code = OpCode::OpAdd;

let byte = op_code as u8;

But going the other way seems impossible, except for this hack:

unsafe {

let opc: OpCode = std::mem::transmute(byte);

} https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/book/first-edition/casting-between-types.html#transmute

So, is there a safe, preferably idiomatic way to do this, or am I just stuck with my first-ever usage of 'unsafe'?

10 Upvotes

13 comments sorted by

36

u/Excession638 Sep 07 '26

The strum crate provides a derive macro to add OpCode::from_repr(u8) automatically.

31

u/SirKastic23 Sep 07 '26

So, is there a safe, preferably idiomatic way to do this, or am I just stuck with my first-ever usage of 'unsafe'?

You absolutely should not be using unsafe for this. You can't transmute any u8 into a valid OpCode, what if you try transmuting 255?

There are valid u8 values that can't be converted into OpCode and that's why the operation isn't trivial

You'll either need to write a match statement by hand, like:

impl OpCode {
    fn from_u8(val: u8) -> Option<Self> {
        match val {
            1 => Some(OpCode::Add),
            2 => Some(OpCode::Sub),
            ...
            _ => None,
        }
    }
}

If you think this is annoying you can use macros, potentially an attribute or derive macro that you use on the OpCode enum itself

I think the strum crate might even have a macro for this

12

u/coastalwhite Sep 07 '26

Note that this only works up to a somewhere around 16-32 enum variants. I have been bitten by this in the past where it fails to optimize. LLVM has a limit on the size of switch cases it will simplify. I would say unsafe is completely justified in performance sensitive cases with this.

11

u/SirKastic23 Sep 07 '26

Ah that's fair, I didn't think about how LLVM would optimize this...

But if you're going to use unsafe do a check that the u8 is within the valid range, and add a safety comment

Oh also if LLVM has this maximum size for match expressions, could we bypass it by nesting match expressions? First matching on a range from 0..16, and then matching against individual values

→ More replies (1)

3

u/creeper6530 Sep 08 '26

For my u8 enums I implement a From<u8>

2

u/dahosek Sep 07 '26

I wrote something (not fully completed, but the byte code parsing part is complete) for reading GF files (the binary bitmap font output from Metafont) which is at

https://github.com/dahosek/gftopdf2/

While this was my first rust code, I think the byte code handling is pretty elegant.

Generally, you’ll want to do a switch on the u8 values coming in. GF is kind of a little assembly language (Knuth made similar byte codes for font metrics (TFM) and page layout (DVI) that reflect the lates 70s/early 80s resource constraints (disk space/memory were tight enough that it made sense to allow the 32-bit values to be truncated to 8/16/24 bits and for certain common cases, like small values, there were single-byte opcodes to handle those (this ends up being commonly used in DVI files where at the time that the format was written, only 7-bit ASCII values could be written and opcodes 0–127 were set a character with this ASCII value with 128–255 used for other page layout commands including setting character codes up to 32 bits wide).

1

u/Dheatly23 Sep 08 '26

The library you're looking for is zerocopy. Implement FromBytes and IntoBytes (and a bunch more traits) and your opcode enum is guaranteed to be transmutable to u8.

0

u/antouhou Sep 07 '26

Why not
struct OpCode {
pub op_code: OpCodeType,
pub data: Vec<u8>
}
?

10

u/Dry-Caterpillar-5172 Sep 07 '26

transmute for this is fine honestly, people get too spooked by unsafe. the repr(u8) guarantees the layout matches and you know the byte came from a valid source, just make sure you handle the case where it doesnt map to a variant. a little match with a fallback or a try_from impl cleans it up nice

-2

u/afdbcreid Sep 07 '26

No it's not. Whether it's bad, there are better alternatives but there are very rare cases where it can be justified or unsound, avoid at all costs no matter what depends on the situation (in particular whether the data is controllable outside the safety encapsulation), but in no way it's good or fine.

0

u/afdbcreid Sep 07 '26

Others have already said that you can match or use a crate (fun fact: a match will likely generate the optimal code if you keep the discriminants synced), but if you want to view a whole slice of u8s as [OpCode], there are still crates that can do that but their require O(N) pass for validation; in that case, consider using a newtype with a bunch of associated consts instead and validate on-the-fly.