Writing a Variant to Stream
Recently I came to a case when I had to write an OLE Automation VARIANT type variable to a stream. And, of course, read it back as a fully-fledged VARIANT. The solution had to fully support arrays as well. A VARIANT can be a byte, an integer, a floating point number, a date. A VARIANT can be a string. A VARIANT can be a safe array of VARIANTs, which can in turn be safe arrays, and so on. Start to feel the catch?
COM obviously does it, and does it efficiently. However, the RPC documentation is very silent about actual details. Going around the depths of Microsoft RPC and OLEAUT32.DLL revealed some promising functions - VARIANT_UserMarshal, VARIANT_UserUnmarshal, VARIANT_UserSize, and VARIANT_UserFree. Their definitions were even more promising, as they seemed to do exactly what I wanted to. However, their documentation is so scarce, that it is nothing but a mere excuse for documentation. You can bravely decide that these are undocumented. That didn’t stop me from trying them, but with little success. I managed to write some code, that worked just fine on Windows 2000, but failed on Windows XP with E_INVALIDARG.
Moral of the story - never use undocumented features or you are bound to fail sooner or later.
Then I took a look at MFC, namely class CCOMVariant, tempted by its methods ReadToStream and WriteToStream. Studying the source of MFC very soon brought bitter disappointment - these methods did the simple job, and didn’t support arrays.
Back to the drawing board, I pulled my sleeves and examined closely the VARIANT type. In Delphi, a variant is defined as:
In fact, all languages define VARIANTs like that. A VARIANT is nothing but a structure, which holds information about its type, and its data (for simple value types as integer), or a pointer to a location in memory, where its data is located. Delphi’s libraries and compiler do a lot of hidden dirty work for you to ensure proper memory management of variants.
Solution
Once you realize what a VARIANT actually is, solution becomes obvious. You write the type the VARIANT, and then its data. If the VARIANT is a string or another pointer (yes, if a VARIANT holds a string, it contains a pointer to a buffer in memory where string is located in type BSTR, which closely resembles Delphi’s long strings), you will need to get data from there.
When the VARIANT is an array, you have to store also the dimensions of the array, the type of the elements of the array, and data itself. The array is of VARIANTs, you repeat the step above. If the array is of a simple data type (string does not count as such, remember), you can be smart, and block copy all array data with a simple memory copy operation. This will bring you nice performance.
Points of interest in this solution:
- Strings are copied to a local buffer, and then copied to the stream. This part can be improved by adding some subtle pointer arithmetics.
- When the VARIANT is an array of simple value elements - such as byte, integer, double, date, etc, the array is copied as a bulk. This ensures good performance. Otherwise I have to treat every element separately.
The solution below has the following limitations:
- Does not support IDispatch. It was out of the scope of what I needed, actually, and I am not going to rewrite COM itself!
- Arrays are assumed to be zero-based. This can be overcome by writing a little extra info in the stream.
- Unicode strings are not supported. This can be also easily overcome, but I don’t need it. Sorry, folks.
Next time, I will demonstrate how to deserialize a VARIANT, serialized with the code above, in NET. Viva interoperability!