Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

BinaryIO: Easily Read and Write Binary Streams in a Platform Independent Manner

5.00/5 (1 vote)
19 Mar 2020MIT3 min read 7.4K   147  
BinaryReader and BinaryWriter can be used to parse or write binary files more easily and in a portable manner
BinaryReader and BinaryWriter fill a hole in Microsoft's Base Class Library by providing an easy to use class to read and write binary streams to and from byte arrays or numeric values in a platform independent manner.

Introduction

Microsoft .NET provides a number of reader and writer classes for reading and writing textual data but nothing to read or write binary data other than Stream itself. Sure, they provide BitConverter but it's not very platform neutral and does not manage a cursor. This article and accompanying code aims to fill that gap in the .NET framework.

Update: Now supports float and double values

Update 2: Added BinaryWriter, changed library name

Conceptualizing this Mess

Typically, when reading or writing from or to a binary file, you frequently deal with numeric values which may be several bytes in length and encoded differently than the machine you're running on expects. You'll frequently run into cases where you must reorder the bytes that make up a word, dword or qword, float or double. This class makes reading and writing numeric values to or from a stream in any byte order very easy. We can use the BinaryReader class and its derivatives, BinaryStreamReader and BinaryCollectionReader to read values in any byte order we need. Similarly, we can use BinaryWriter and its derivatives, BinaryStreamWriter and BinaryCollectionWriter to write to a Stream or to a collection of bytes, respectively.

Coding this Mess

Included is a small demo that simply reads a few values from the included MIDI file which I downloaded from one of those ringtone MIDI sites:

C#
// open the MIDI file
using(var br = new BinaryStreamReader(@"..\..\GORILLAZ_-_Feel_Good_Inc.mid"))
{
    // check for the FourCC code at the beginning of the file
    if("MThd"!=br.ReadFixedString(4,Encoding.ASCII))
        throw new ApplicationException("The file is not a MIDI file");
                
    // read a big-endian 32-bit integer length from the file
    var len = br.ReadInt32BE();
    // now read that many bytes into a buffer
    var ba = br.ReadBytes(len);
    // create a new binary reader over ba
    // not necessary, but for illustrative purposes:
    using (var br2 = new BinaryCollectionReader(ba))
    {
        // read the file type as a big-endian 16-bit short
        Console.WriteLine("MIDI File Type: " + br2.ReadInt16BE().ToString());
        // read the track count as a big-endian 16-bit short
        Console.WriteLine("MIDI Track Count: " + br2.ReadInt16BE().ToString());
        // read the timebase as a big-endian 16-bit short
        Console.WriteLine("MIDI TimeBase: " + br2.ReadInt16BE().ToString());
    }
}

Hopefully, the comments will clarify what it's doing. MIDI files are stored in big-endian format while Intel machines are little-endian. We therefore use ReadXXXXBE() - note the BE suffix - to read a big-endian value. Read methods sufficed with LE meanwhile, read things in little endian form, while ReadXXXX() with no suffix reads a value in your platform's native byte order. Using the correct method will ensure that the value is translated to the appropriate byte order for your platform as necessary. One thing to note above is the creation of an additional reader over a byte array. This didn't have to be implemented that way as the comment says, but it illustrates a technique in that it uses a nested reader to safely read a chunk or substream of values. This can be useful in cases where a file is divided into different chunks or substreams, like a MIDI file is. However, the above was so simple we didn't strictly need to do it that way, as I said.

I haven't covered writing in the demo because it would make the demo project a lot more involved to write MIDI files. The writing is the opposite of reading, and the APIs are mirror images of one another, so it should be easy to work out. Everything is documented as well.

This library could use plenty of additional read and write methods, such as read and write methods for decimals, zero-terminated strings, etc. I'm leaving that as an exercise for the reader.

History

  • 19th March, 2020 - Initial submission
  • 19th March, 2020 - Updated to support float and double values
  • 19th March, 2020 - Updated to add BinaryWriter and derivatives. Changed the name of the library

License

This article, along with any associated source code and files, is licensed under The MIT License