Reputation: 5543
The COM VARIANT
type is defined using the tagVARIANT
structure like this:
typedef struct tagVARIANT {
union {
struct {
VARTYPE vt;
WORD wReserved1;
WORD wReserved2;
WORD wReserved3;
union {
LONGLONG llVal;
LONG lVal;
BYTE bVal;
SHORT iVal;
FLOAT fltVal;
DOUBLE dblVal;
VARIANT_BOOL boolVal;
VARIANT_BOOL __OBSOLETE__VARIANT_BOOL;
SCODE scode;
CY cyVal;
DATE date;
BSTR bstrVal;
IUnknown *punkVal;
IDispatch *pdispVal;
SAFEARRAY *parray;
BYTE *pbVal;
SHORT *piVal;
LONG *plVal;
LONGLONG *pllVal;
FLOAT *pfltVal;
DOUBLE *pdblVal;
VARIANT_BOOL *pboolVal;
VARIANT_BOOL *__OBSOLETE__VARIANT_PBOOL;
SCODE *pscode;
CY *pcyVal;
DATE *pdate;
BSTR *pbstrVal;
IUnknown **ppunkVal;
IDispatch **ppdispVal;
SAFEARRAY **pparray;
VARIANT *pvarVal;
PVOID byref;
CHAR cVal;
USHORT uiVal;
ULONG ulVal;
ULONGLONG ullVal;
INT intVal;
UINT uintVal;
DECIMAL *pdecVal;
CHAR *pcVal;
USHORT *puiVal;
ULONG *pulVal;
ULONGLONG *pullVal;
INT *pintVal;
UINT *puintVal;
struct {
PVOID pvRecord;
IRecordInfo *pRecInfo;
} __VARIANT_NAME_4;
} __VARIANT_NAME_3;
} __VARIANT_NAME_2;
DECIMAL decVal;
} __VARIANT_NAME_1;
} VARIANT;
Normally when the caller wants to use the data inside a Variant, it uses the VARTYPE vt
flag to see what kind of data is stored, and ultimately how those 1s and 0s should be interpreted.
What happens then when a DECIMAL
is stored in the Variant; the definition lies outside the struct
containing vt
, so how does the caller determine whether there's a valid type flag or just some bytes of the Decimal? The Decimal takes 12* 14 bytes to store and the Variant can hold 16, so possibly this information is leveraged, but isn't what's stored in the spare 2 bytes of the smaller member of a union undefined behaviour?
Upvotes: 4
Views: 520
Reputation: 112
If it helps an explaination of the Decimal type from The Decimal Data Type also some handy Decimal conversion functions at the link.
The structure for the decimal type within the variant type.
Public Type DecimalType ' (when sitting in a Variant)
vt As Integer ' Reserved, to act as the variable Type when sitting in a 16-Byte-Variant. Equals vbDecimal(14) when it's a Decimal type.
Base10NegExp As Byte ' Base 10 exponent (0 to 28), moving decimal to right (smaller numbers) as this value goes higher. Top three bits are never used.
Sign As Byte ' Sign bit only (high bit). Other bits aren't used.
Hi32 As Long ' Mantissa.
Lo32 As Long ' Mantissa.
Mid32 As Long ' Mantissa.
End Type
A interesting quote from the link regarding LenB of a decimal type:
A Decimal in a Variant is wholly contained within the variant, 14 bytes for the Decimal data, and 2 bytes for the Variant to indicate that it's holding a Decimal type. So, always 16 bytes for a Variant/Decimal.
However, here's a quote from MSDN:
If varname is a Variant, Len treats it the same as a String and always returns the number of characters it contains. And that's for the Len function, but its help doubles for the LenB function as well. So, LenB is implicitly converting the Variant/Decimal to a string before it's telling you the number of bytes.
EDIT1: I didn't test, but I bet if the Variant (containing a Decimal) is in a UDT, it'll just count 16 bytes for it.
The LenB of a Decimal type was throwing me off a bit until reading the above.
Upvotes: -1
Reputation: 34058
This is an intriguing question. Sadly I haven't been able to find any firm documentation about this. I can make some inferences from a bit of thinking and experimentation.
Notwithstanding the official documentation and type definitions in headers -- a DECIMAL stored in a VARIANT does appear to use the bytes of the DECIMAL wReserved
member for the overlapping vt
VARIANT member. Therefore, a DECIMAL in a VARIANT is identified the same way as any other VARIANT type by looking at the vt
member.
I present two empirical proofs.
1) I compiled a VB6 program to store a DECIMAL in a VARIANT (Native Code, No Optimizations, Generate Symbolic Debug Info). Then I used an old version of WinDbg to inspect the bits of the variable (the current versions of WinDbg are not compatible with VB6's older PDB format - I guess I could have tried using VC6 for this instead but didn't think about it).
Dim v As Variant
v = CDec(24)
Inspecting v with WinDbg, I obtained the following layout for the v
variable:
0e 00 00 00 00 00 00 00 18 00 00 00 00 00 00 00
----- ----- ----------- -----------------------
| | | |
| | | Lo64
| | Hi32
| signscale
wReserved
(but note it's the same as v.vt == VT_DECIMAL)
Ok, VB6 is not above cheating in weird places, and it always seems strange that Microsoft would not expose Decimal as a full type (for some reason you cannot declare a variable of type Decimal in VB6; it has to be stored in a Variant. The documentation for Dim
makes it sound like they intended to support Decimal and had to pull it out for some reason). So it's possible this is just a VB6 cheat. However:
2) I tested to see what the COM API would do if I asked it to put a DECIMAL in a VARIANT. For kicks, I used VC6++ to test this:
VARIANT s;
VARIANT t;
VariantInit(&s);
VariantInit(&t);
V_VT(&s) = VT_I4;
V_I4(&s) = 24;
HRESULT hr = VariantChangeType(&t, &s, 0, VT_DECIMAL);
I confirmed that hr
was S_OK
. If it was formally illegal to store a DECIMAL by value in a VARIANT, I would have expected an error HRESULT. Instead, the layout matched my experience with VB6:
t
as {24 VT_DECIMAL}
t.vt
member was set to 14 (which is VT_DECIMAL)t.decVal
member was listed as wReserved == 14; Lo64 == 24; Hi32 == 0Therefore, despite what the header declaration of VARIANT implies, the vt member can and should be used to determine when a VARIANT contains a DECIMAL. In fact, if you never inspected the declaration of VARIANT in detail you would never know that DECIMAL is treated differently.
The question I am left with is "why not just make DECIMAL fit it in the union like everybody else?".
It might be hard to produce the full answer without knowing the complete history of VARIANT and DECIMAL; but the key is probably not in vt
but in wReserved1
, wReserved2
and wReserved3
.
DECIMAL appears to be a later addition to VARIANT. Kraig Brockschmidt's classic book "Inside Ole" (2nd Edition, dated 1995) gives the declaration of VARIANT but does not mention DECIMAL as one of the options. That means that DECIMAL as a VARIANT option was added at some point afterward. No later than Visual C++ 6 (1998), DECIMAL was already available as a VARIANT type.
But the interesting parts of DECIMAL (14 bytes) are too large to fit in the preexisting VARIANT union. DECIMAL needs to use the bytes taken by the three wReservedX
fields (likely originally intended as padding). I'm pretty sure there is no way Microsoft could have redefined the VARIANT union to make the Reserved fields available to the union and to DECIMAL without changing the memory layout and breaking old binaries.
So one theory is that Microsoft needed to add this new 14-byte long type to VARIANT, which couldn't possibly fit on the 8 bytes available to the union. Under this theory, the current layout of VARIANT would be a way to sneak in DECIMAL at the binary level without breaking the original declaration of VARIANT. When compiled, DECIMAL would just be another member of the "union" except that it can overflow into the space of the reserved WORDs.
There might be another quirk. Hans Passant mentions in a comment above that the reserved fields used to contain currency type information. It sounds very feasible but I can't corroborate it because I haven't found any information about older uses of DECIMAL. Assuming that is true, Microsoft would have been constrained on the layout of the preexisting DECIMAL type (i.e. it was impossible to consider sacrificing range to make it fit as a conventional member). Additionally, they would have had to decide they could dispense with the "currency type" information in exchange for making DECIMAL work in VARIANTs (or they might have already discarded the currency type information earlier, or for a different reason). I can't tell without more information about how DECIMAL was used before they were added as a VARIANT type.
Upvotes: 3