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

Enum Streaming Code Auto-Gen (Binary)

4.18/5 (3 votes)
2 Nov 2011CPOL5 min read 19.1K   106  
Using LINQ Expression Trees to auto generate binary streaming methods for enums.

Contents

Introduction

Do you explicitly deal with binary streams (using BinaryReader\Writer)? Do you also deal with enum data within those streams? Then chances are you will want to read this article. I will be covering a problem I had to solve for auto-generating code to stream enum values to\from a binary stream (it hides casting and such). The solution I came up with allows me to specify the enum's size within the stream, even if that size is different from the enum's implementation. For example, the System.TypeCode enum is implemented with an int as its underlying type. However, with this system, I can tell the code generator to treat the value it streams to\from a source as a byte instead of an int.

Background

Not too long ago, I was porting some code from a C++/CLR project to C#. The project deals with object-code and PE-related files so it was originally more beneficial to implement some of it in C++/CLR (since I had direct access to code definitions, e.g., IMAGE_SYMBOL). Part of the original C++ code was a template'd function I wrote for streaming enum values:

C++
template<typename TEnum> 
TEnum /*EndianReader_*/StreamEnumUInt8(KSoft::IO::EndianReader^ s)
{
    return cli::safe_cast<TEnum>( s->ReadByte() );
}
template<typename TEnum> 
void /*EndianWriter_*/StreamEnumUInt8(KSoft::IO::EndianWriter^ s, TEnum value)
{
    s->Write(cli::safe_cast<byte>( value ));
}

and so on for the various integer types. Note: EndianReader\Writer are specialized implementations of BinaryReader\Writer that support reading from streams in different endian byte orders other than the host system's endian format.

As we all know, C# doesn't support templates (I'm with the party who dislikes this fact, sorry), so this solution wasn't going to just port over with a few changes. Having just recently finished my first article here at CodeProject which covers auto-generating code for accessing private members, I put my Linq.Expressions hat back on and started thinking how I could solve this problem using Expression Trees.

At first, I was wanting to just have a generic class where I provide an enum type as a generic parameter and have it automatically figure out the integer type to use when streaming to\from a source based on the enum's underlying type. However, I saw this as not only more trouble than I thought worth, but it also meant that the generic class would rely entirely on the enum's type definition. Not very friendly for future changes, or code which you don't have the source to, or in the strange cases where the enum is stored in different sizes in different locations.

So I came up with the generic class EnumBinaryStreamer<TEnum, TStreamType>. TEnum is obviously the parameter for the enum type, and TStreamType is the integer-based type (e.g., uint\System.UInt32) which will be used when streaming the TEnum value to\from a binary source.

Using the code

There are two ways to use the system: via the full type name (or an alias) or via an interface instance. The EnumBinaryStreamer class\instances expose three public methods, Read (two overloads), and Write, which is all anyone really cares about (unless you're curious about the internals).

I create and name an alias in the following way:

C#
using TypeCodeStreamer8 = EnumBinaryStreamer<System.TypeCode, byte>;
using TypeCodeStreamer32 = EnumBinaryStreamer<System.TypeCode, int>;
using TypeCodeStreamer64 = EnumBinaryStreamer<System.TypeCode, long>;

You can use the system with an interface instance:

C#
var streamer_instance = EnumBinaryStreamer<System.TypeCode, int>.Instance;
// or
var streamer_instance = TypeCodeStreamer32.Instance;

// NOTE: streamer_instance's type is IEnumBinaryStreamer<TEnum, TStreamType>

Okay, so we have a way to access the system's interface. How do we now use it? The following is taken from EnumStreamerTest.cs (see the article's source files):

C#
using (var ms = new System.IO.MemoryStream())
using (var br = new BinaryReader(ms))
using (var bw = new BinaryWriter(ms))
{
    const System.TypeCode kExpectedValue = System.TypeCode.String;
    var value = kExpectedValue;

    // Using an alias to write an enum value to the BinaryWriter object 'bw'
    TypeCodeStreamer32.Write(bw, value); // -- Usage Example --
    ms.Position = 0;
    // Using an alias to read an enum value from the BinaryReader object 'br'
    TypeCodeStreamer32.Read(br, out value); // -- Usage Example --

    Assert.IsTrue(value == kExpectedValue);

    //////////////////////////////////////////////////////////////////////////
    // Test the instance interface
    var streamer_instance = TypeCodeStreamer32.Instance;
    ms.Position = 0;

    // Using an interface instance to write an enum value to the BinaryWriter object 'bw'
    streamer_instance.Write(bw, value); // -- Usage Example --
    ms.Position = 0;
    // Using an interface instance to read an enum value from the BinaryReader object 'br'
    streamer_instance.Read(br, out value); // -- Usage Example --

    Assert.IsTrue(value == kExpectedValue);
}

All you need is a BinaryReader\Writer object and an enum value, and you're pretty much set to use the system! All the nitty gritty is checked and handled by the system's code. Note: I use CodeContracts for sanity checks. If you can't use CodeContracts (Mono.NET?) in your code, then you'll have some extra work ahead of you (you'll have to re-write the parameter validation).

Note: Internally (in EnumBinaryStreamerBase's InitializeMethodDictionaries method) I reference Omer Mor's EnumComparer class, but it isn't needed for the code to work. You should just need to remove the EnumComparer.Instance argument passed to the Dictionary ctor in the InitializeMethodDictionaries method where it's used.

Behind the code

The system is made up of four classes (EnumUtils, EnumBinaryStreamerBase, EnumBinaryStreamerBase.StreamType, EnumBinaryStreamer) and one interface (IEnumBinaryStreamer).

EnumUtils houses some utilities related to the underlying implementation of enum types and the various supported underlying integer types. Code found here along with its comments should be self explanatory.

EnumBinaryStreamerBase and its internal StreamType class handle the grunt work of building the MethodInfo references to the various BinaryReader\Writer methods that stream integer data. The MethodInfo is needed in the code generation process. I defined it as a separate class from EnumBinaryStreamer<TEnum, TStreamType> so there wouldn't be an instance of the static MethodInfo dictionaries and such for every implementation of EnumBinaryStreamer. Since it is a generic class, each TEnum causes a new implementation to be allocated and initialized, so in the end, it would have just been repeated processing and memory.

EnumBinaryStreamer<TEnum, TStreamType> is where all the sanity checking and code generation is performed. See the cctor (static ctor) for the validation checks for the type parameters. See GenerateReadMethod and GenerateWriteMethod for the code generation logic and documentation. EnumBinaryStreamer explicitly implements the IEnumBinaryStreamer interface so you can use an instance to access the Read\Write methods (in case type names or aliases start to become too long for pretty code).

Conclusion/Closing thoughts

I realize I didn't dive too much into the backing Linq.Expressions code in this article. This is intentional. I believe the logic behind the code generation can be understood by reading the code along with the supporting comments. I tried explaining the Linq.Expression details in depth for my previous article. When it came time to writing this article, I really didn't want to regurgitate what I believe to be simple Expressions code.

Does this make me a bad article author? I hope not. A lazy one? Probably. I realize that someone with very little understanding of .NET Reflection would probably struggle with the internals of this code. However, that is why I labeled this article as "intermediate". As for the Linq.Expression code, all you really need is to refer to MSDN. With all of that, along with my comments, understanding the implementation code shouldn't be a problem. If, however, anyone has issues, please don't hesitate to leave a message and I'll try to give you a respectable answer :p!

References

History

  • 2011 Nov. 1 - Initial article creation.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)