Binary file formats often use very compact representations of data, from single bytes to individual bits (or bit fields) in a byte. Converting between binary data and Rust bytes is essential, to be able to write more understandable programs that are able to both parse the binary format and to generate it.
For example, a certain digital MIDI synthesizer uses digital LFO (Low Frequency Oscillator) to generate control waveforms that affect the pitch or amplitude of the sound. The LFO can generate five different waveforms, which can be represented using a Rust enumerated type:
#[derive(Debug, Eq, PartialEq, Copy, Clone)] pub enum Waveform { Triangle, Square, Sawtooth, Sine, Random }
In this case the waveforms are represented in the binary data as single bytes, with values that correspond to those assigned by default to the enum members: 0, 1, 2, 3, and 4. So, when any of the values 0x00, 0x01, 0x02, 0x03, or 0x04 appear in a certain position in the binary file, we know that it means an LFO waveform of the corresponding type.
To be able to construct a new instance of Waveform
based on a byte (in Rust, the
u8
data type) we need to consider all the different possibilities:
let b: u8 = 0x02; let w = match b { 0x00 => Waveform::Triangle, 0x01 => Waveform::Square, 0x02 => Waveform::Sawtooth, 0x03 => Waveform::Sine, 0x04 => Waveform::Random, _ => panic!("Waveform byte out of range: should be 0...4, was {}", b), }; println!("{:?}", w);
If you run this with a value of b
that falls in the range, everything is fine.
But if you initialize b
with a value that is out of the desired range, the program
will panic:
let b: u8 = 0x08;
% cargo run --quiet thread 'main' panicked at 'Waveform byte out of range: should be 0...4, was 8', src/main.rs:30:14 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
There are two problems with this approach. First, the byte value could be out of range if the file is corrupted, or if there is an error in the program, but we don't know which, so maybe a panic is a bit too much here; we might want to recover from the situation somehow.
Second, the code to handle
the initialization is quite verbose, and you
wouldn't want to repeat it every time you need to initialize a Waveform
instance from
a u8
.
We could try to mitigate the second problem by implementing the From
trait
for Waveform
, removing duplication:
impl Fromfor Waveform { fn from(item: u8) -> Self { match item { 0x00 => Waveform::Triangle, 0x01 => Waveform::Square, 0x02 => Waveform::Sawtooth, 0x03 => Waveform::Sine, 0x04 => Waveform::Random, _ => panic!("Waveform byte out of range: should be 0...4, was {}", item) } } }
Now we can instantiate a Waveform
simply by saying:
let w = Waveform::from(b);However, this only moves the
panic!
macro out of the call site, so the first
problem still remains.
There is a handy crate called num_enum which
contains helpers for turning
primitive values into enumerations and back. To use it, add the crate into your Cargo.toml
file:
[dependencies] num_enum = "0.5.7"
Then add the required imports to your program:
use num_enum::TryFromPrimitive; use std::convert::TryFrom;
Finally, derive TryFromPrimitive
for the Waveform
type, and also
specify a primitive
representation for it with #[repr(u8)]
:
#[derive(Debug, Eq, PartialEq, Copy, Clone, TryFromPrimitive)] #[repr(u8)] pub enum Waveform { Triangle, Square, Sawtooth, Sine, Random, }
Now we can use try_from
to instantiate a Waveform
:
let b: u8 = 0x02; let w = Waveform::try_from(b); println!("{:?}", w);
% cargo run --quiet Ok(Sawtooth)
So far, so good. If you pass in a byte that does not have a corresponding enum value, you will
get an Err
back:
% cargo run --quiet Err(TryFromPrimitiveError { number: 8 })
Clearly, we need to handle the result better if we want to recover from the error:
if let Ok(w) = Waveform::try_from(b) { println!("{:?}", w); } else { println!("Waveform byte out of range: should be 0...4, was {}", b); }
Once you have a valid Waveform
instance, turning it back into a byte is also very easy
with a few initial definitions:
use num_enum::{TryFromPrimitive, IntoPrimitive}; use std::convert::TryFrom; #[derive(Debug, Eq, PartialEq, Copy, Clone, TryFromPrimitive, IntoPrimitive)] #[repr(u8)] pub enum Waveform { Triangle, Square, Sawtooth, Sine, Random, }
Now you can use into()
:
let b2: u8 = waveform.into(); println!("{}", b2);
You should see the result 2
printed out in the console.