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:
template<typename TEnum>
TEnum StreamEnumUInt8(KSoft::IO::EndianReader^ s)
{
return cli::safe_cast<TEnum>( s->ReadByte() );
}
template<typename TEnum>
void 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:
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:
var streamer_instance = EnumBinaryStreamer<System.TypeCode, int>.Instance;
var streamer_instance = TypeCodeStreamer32.Instance;
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):
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;
TypeCodeStreamer32.Write(bw, value);
ms.Position = 0;
TypeCodeStreamer32.Read(br, out value);
Assert.IsTrue(value == kExpectedValue);
var streamer_instance = TypeCodeStreamer32.Instance;
ms.Position = 0;
streamer_instance.Write(bw, value);
ms.Position = 0;
streamer_instance.Read(br, out value);
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.