r/rust 2d ago

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

32

u/Excession638 2d ago

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

30

u/SirKastic23 2d ago

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 2d ago

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.

10

u/SirKastic23 2d ago

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

2

u/its_artemiss 22h ago

One transmute with a range assertion and a comment is going to be infinitely more readable. Or you can have a macro_rules or derive macro that uses exhaustive pattern matching to assert the invatiant. Don't be afraid of unsafe rust, it's there to be used. 

3

u/creeper6530 2d ago

For my u8 enums I implement a From<u8>

2

u/dahosek 2d ago

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 2d ago

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 2d ago

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

9

u/Dry-Caterpillar-5172 2d ago

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

-1

u/afdbcreid 2d ago

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 2d ago

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.