[][src]Crate endio

Simple and ergonomic reading and writing of binary data.

std::io::{Read, Write} only allow reading and writing of raw bytes. This can be cumbersome when trying to read/write data, e.g. integers, as they have to be converted from/to bytes first. This crate allows direct reading/writing of data types, making this as easy as

let value: u8 = reader.read()?;

and

writer.write(42u8);.

Automatic endianness conversion

When working with integers consisting of multiple bytes, two kinds of byte ordering are possible, big and little endian. This crate can automatically convert integers from/to the desired endianness when they're read/written, without you having to specify the endianness for every read/write call. This works by making the endianness part of the type of the read/write implementor through different traits. This crate will never try to guess endianness, you'll always have to specify it explicitly (but only once).

You aren't bound to the specified endianness for the entirety of the I/O. If you need to work with data where the endianness changes midway through, you can explicitly override the endianness with the _be-/_le -suffixed methods.

Entirely trait-based I/O

This crate is entirely trait-based, which means it adds zero state at runtime. All the information needed is encoded at the type level. All functionality is delegated to (de-)serialization code.

Zero-cost abstractions

As this crate is entirely trait-based, the compiler will typically aggressively inline and optimize out all calls to this crate. The only thing remaining will be the (de-)serialization code, which is also necessary without the use of this crate. Therefore using this crate will not result in any penalty to your code, neither in speed nor memory use.

When I compared code using byte-level I/O from this crate to code using the equivalent raw std::io functionality in a disassembler, compiled in release mode, I only found found a difference of a few opcodes, and all function calls to this crate had been completely optimized away.

I haven't yet done any serious benchmarks, or checked the disassembly of all types and features, but these initial results look promising.

Extendability: Reading & writing your own types

You can take advantage of this crate's features and the read/write methods by implementing this crate's traits for your own types. No distinction is made between user types and types for which (de-)serialization is already provided by this crate, and you will be able to use read and write for your own types just like for primitive types.

You can write (de-)serializations that differ depending on endianness, or (de-)serializations that don't differentiate and instead use the automatic endian inference of this crate to delegate endian differentiation to sub-structs.

You're not limited to std::io::{Read, Write} or this crate's abstractions when writing (de-)serializations, you can also use other functionality through appropriate trait bounds. For example, it's also possible to write a (de-)serialization that uses std::io::Seek in its code.

Simple migration

If you were using raw std::io::{Read, Write} with manual (de-)serialization code before, or a crate providing abstractions on top of {Read, Write}, the migration towards using this crate is extremely straightforward. Simply swap out the std::io traits with the endian-specific traits from this crate, and you'll have access to the direct read/write methods. You don't need to write any extra code, everything that implements std::io::Read or std::io::Write will automatically implement the endian-specific traits.

(De-)serializations for Rust's primitive types are already implemented, if it makes sense to implement them. For example, isize and usize aren't implemented, since they are inherently variable-sized and therefore can't have a portable byte representation. However, the other integral and floating point types are implemented, and if you just want to read/write some simple primitive types, using the read/write methods will Just Work™.

See the examples below for some typical I/O using this crate.

Examples

Here are two typical ways to use this crate:

Read data from bytes in memory, in little endian:

// This will make the read calls use little endian.
use endio::LERead;

// Works with any object implementing std::io::Read.
let mut reader = &b"\x2a\x01\x2c\xf3\xfe\xcf"[..];

let a: u8   = reader.read().unwrap();     // Reads in little endian (specified by trait).
let b: bool = reader.read().unwrap();     // Deserialization code is automatically inferred.
let c: u32  = reader.read_be().unwrap();  // Reads in forced big endian.

// The results are already converted into the appropriate types and ready for use.
assert_eq!(a, 42);
assert_eq!(b, true);
assert_eq!(c, 754187983);

Write data to a vector of bytes, in big endian:

// This time we'll use big endian.
use endio::BEWrite;

// Vec implements std::io::Write.
let mut writer = vec![];

writer.write(42u8);          // Directly specifying values works fine.
writer.write(true);          // Everything is automatically converted to bytes.
writer.write_le(754187983);  // The trait endianness can be overwritten if necessary.

// Done!
assert_eq!(writer, b"\x2a\x01\xcf\xfe\xf3\x2c");

More examples and explanations on how this crate works are included in the documentation of the interfaces. There are also examples on how to implement your own types.

Getting started

To conduct I/O you use the traits BERead & BEWrite, or LERead & LEWrite. Choose BERead & BEWrite for big endian I/O, and LERead & LEWrite for little endian I/O. This will give you the read/write methods on your structs. read returns values of your desired type, and write accepts values as a parameter. The deserialization to be used and the type to be returned are handled through type inference, so most of the time you won't even need to annotate the type explicitly.

You can read and write your own types by implementing Serialize/Deserialize. See their documentation for details.

Structs

BigEndian

Only necessary for custom (de-)serializations.

LittleEndian

Only necessary for custom (de-)serializations.

Traits

BERead

Use this to read in big endian.

BEWrite

Use this to write in big endian.

Deserialize

Implement this for your types to be able to read them.

ERead

Only necessary for custom (de-)serializations.

EWrite

Only necessary for custom (de-)serializations.

Endianness

Only necessary for custom (de-)serializations.

LERead

Use this to read in little endian.

LEWrite

Use this to write in little endian.

Serialize

Implement this for your types to be able to write them.