Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

[solved] handle VARIANT structures


  • Please log in to reply
13 replies to this topic
maul.esel
  • Members
  • 790 posts
  • Last active: Jan 05 2013 09:26 PM
  • Joined: 28 Feb 2011
Hi all,

In CCF, I used to wrap structures used into classes, e.g. RECT. However, when it comes to VARIANT / VARIANTARG, I'd like to avoid this, because it's quite a lot of work and I'm not sure how to handle it exactly.

I thought ComObjType() may be able to help me, but I'm no longer sure. For example, it seems the VT_* values supported are limited. Secondly, I need to put the data in a structure (not a pointer to the data, but the data itself). For an example see CUSTDATAITEM. I thought of using the RtlMoveMemory function to copy the data. However, I'd need the size of the structure which may vary.

Can anybody please help me? Any suggestions how to handle it? Any ideas on how to determine the size?

Regards
maul.esel
Join the discussion on The future of AutoHotkey
Posted Image Visit me on github Posted Image
Win7 HP SP1 64bit | AHK_L U 64bit

Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006

I thought ComObjType() may be able to help me,

How? In its single-parameter mode, it just returns the variant type of the given wrapper object.

For example, it seems the VT_* values supported are limited.

The documentation for ComObjType lists all variant types which are documented by Microsoft as "may appear in a VARIANT". I think that VT_DECIMAL is the only one which is not supported in a wrapper object, since it is a 16-byte structure. (It is defined as overlapping with VARIANT::vt, but the first two bytes in the DECIMAL structure are actually "reserved".)

Secondly, I need to put the data in a structure

Aside from VT_DECIMAL, a VARIANT can be formed by writing the value returned by ComObjType into the first 8 bytes and the value returned by ComObjValue into the last 8 bytes.

I thought of using the RtlMoveMemory function to copy the data. However, I'd need the size of the structure which may vary.

A VARIANT structure is always exactly 16 bytes. However, sometimes the value it contains is a pointer to more data.


I'm not quite sure what "handling" it is that you need to do. You can take advantage of AutoHotkey's own handling of VARIANT by creating a ComObjArray of 1 VT_VARIANT. For example, see ComVar() in the ComObjActive() examples.

maul.esel
  • Members
  • 790 posts
  • Last active: Jan 05 2013 09:26 PM
  • Joined: 28 Feb 2011

The documentation for ComObjType lists all variant types which are documented by Microsoft as "may appear in a VARIANT".

Damn, I didn't even think about it. So not AHK is "limited" but the VARIANT structure.

Aside from VT_DECIMAL, a VARIANT can be formed by writing the value returned by ComObjType into the first 8 bytes and the value returned by ComObjValue into the last 8 bytes.

That sounds clever. :) But VARTYPE is defined as "unsigned short". So why the first 8 bytes? What happens to those "reserved" WORDs? (Edit 3: nevermind, I figured it out) I also need to think about VT_DECIMAL. :?

A VARIANT structure is always exactly 16 bytes. However, sometimes the value it contains is a pointer to more data.

Thanks. The definition is somewhat confusing to me. And I just realized yesterday that a union's size isn't variable but equal to the size of the largest member. :oops:

You can take advantage of AutoHotkey's own handling of VARIANT by creating a ComObjArray of 1 VT_VARIANT. For example, see ComVar() in the ComObjActive() examples.

I'll look into it.

One thing that still isn't clear to me: what's the difference between VARIANT and VARIANTARG? To me it seems it's the same. However, why are there 2 names then?

I'm not quite sure what "handling" it is that you need to do.

That's what I'm thinking about, too. ;-)
For a wrapper method that needs to pass a VARIANT to a COM method / put it in a struct,[*:23nyuuih]should I require the user to pass a pointer to a VARIANT?
[*:23nyuuih]Or is it possible to determine the VT_* type of an AHK variable? (I doubt this, e.g. how to differentiate between an IDispatch pointer and an IUnknown pointer?)
[*:23nyuuih]Or should I require the user to pass the value and a VT_* constant?
Edit: Do you know a "convenient way" to build a DECIMAL structure from an AHK "floating-point" variable?

Edit 2: Would the following code be the correct way to build a VARIANT:
BuildVariant(value, vt)
{
	local var
	static VT_DECIMAL := 14

	VarSetCapacity(var, 16, 00)
	if (vt == VT_DECIMAL)
	{
		; put DECIMAL in structure
	}
	else
	{
		NumPut(ComObjValue(value), &var, 12, "UInt64")
	}
	NumPut(vt, &var, 00, "Short")
}

Join the discussion on The future of AutoHotkey
Posted Image Visit me on github Posted Image
Win7 HP SP1 64bit | AHK_L U 64bit

Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006

One thing that still isn't clear to me: what's the difference between VARIANT and VARIANTARG?

Basically, the difference is that one has a longer name than the other. Both type definitions are based on the same struct definition (tagVARIANT).

VARIANTARG describes arguments passed within DISPPARAMS, and VARIANT to specify variant data that cannot be passed by reference.
Source: VARIANT structure

should I require the user to pass a pointer to a VARIANT?

I say no. Convert whatever value (string, integer or float) using the ComObjArray technique I mentioned earlier. If a user wants to pass a specific type, they can use ComObjParameter() as with any other COM method. (This is handled automatically by ComObjArray.)

Or is it possible to determine the VT_* type of an AHK variable?

If you mean "AHK value" and that includes any value that can be stored in a variable, then no, because only COM wrapper objects have VT_* types. However, you can determine which type AutoHotkey converts the value into after storing it in a ComObjArray of variants. With the ComVar example, you can use the following:
MsgBox % NumGet(ComObjValue(var.ref), 0, "ushort")
Note that the value returned by ComObjValue in this case is a pointer to a VARIANT structure.

(I doubt this, e.g. how to differentiate between an IDispatch pointer and an IUnknown pointer?)

ComObjType. However, if you use a ComObjArray to construct the VARIANT, you don't need to differentiate any type from any other type.

Edit: Do you know a "convenient way" to build a DECIMAL structure from an AHK "floating-point" variable?

No. I also don't know why you'd want to convert an AutoHotkey floating-point number (which is equivalent to VT_R8) into VT_DECIMAL.

Edit 2: Would the following code be the correct way to build a VARIANT:

No.

ComObjValue(value) will only return something meaningful if value is a COM wrapper object. If value is a COM wrapper object, you should be using ComObjType(value) in place of vt. Otherwise, value should be an integer and you should put it directly in the structure. For floating-point numbers you would need to change the NumPut type.

An offset of 12 would put the value in the wrong place, in addition to overflowing the structure. Note that if you omit the & (address-of) operator and just pass a variable reference, NumPut detects overflow errors such as this automatically. It is also faster.

What are you constructing the variant from? If an arbitrary value (such as a string or number), I recommend using the ComObjArray approach as it is relatively simple and ensures type conversions are consistent with every other COM method called by script.

maul.esel
  • Members
  • 790 posts
  • Last active: Jan 05 2013 09:26 PM
  • Joined: 28 Feb 2011
I tried to do it with the array technique. My results are:
BuildVariant(value)
{
	array := ComObjArray(0xC, 1)
	array[0] := value
	DllCall("oleaut32\SafeArrayAccessData", "ptr", ComObjValue(array), "ptr*", arr_data)
	DllCall("oleaut32\SafeArrayUnaccessData", "ptr", ComObjValue(array))
	return { "ref" : ComObjValue(ComObjParameter(0x4000|0xC, arr_data)), "array" : array }
}
COMStruct(value)
{
	var := BuildVariant(value)

	VarSetCapacity(struct, 16, 00)
	DllCall("RtlMoveMemory", "ptr", &struct, "ptr", var.ref, "UInt", 16)

	SetFormat, Integer, H
	MsgBox % "The vartype is: " NumGet(struct, 00, "UShort")

	; EDIT: insert the code in the other code boxes here
}

COMStruct(1000) ; yields vt = 0x3 - expected
COMStruct("ABC123") ; yields vt = 0x8 - expected
COMStruct(ComObjParameter(VT_UINT := 0xB, 1000)) ; yields vt = 0xB - expected
COMStruct(ComObjParameter(VT_ERROR := 0xA, 12)) ; yields vt = 0xA - expected
It creates a memory block holding the VARIANT structure and reads the vt field from it. To me, it seems to work. If you have any suggestions / corrections, please post them.

Thanks a lot for you support, Lexikos :!: You've helped me a lot :!: Besides that, thanks again for the efforts you put in AHK_L / AHK v2, making this code possible :!:

Edit: 1 question remains: How to handle the opposite case (I have a VARIANT in memory and want to present its value to the AHK user) :?: Does the following code suffice:
; "struct" is the same as in the COMStruct() function in the code above
vt := NumGet(struct, 00, "UShort")
val := NumGet(struct, 08, "UInt64") ; is it important to change the type here or does "UInt64" fit all types?
MsgBox % "The value is: " value := BuildVariant(ComObjParameter(vt, val)).array[0]
The problem with this is the user can't get the type :( I tried building a ComObjParameter() from the value and the "vt", so that the user can use ComObjType() and ComObjValue() on the result, but the latter doesn't work for VT_BSTR.
Join the discussion on The future of AutoHotkey
Posted Image Visit me on github Posted Image
Win7 HP SP1 64bit | AHK_L U 64bit

maul.esel
  • Members
  • 790 posts
  • Last active: Jan 05 2013 09:26 PM
  • Joined: 28 Feb 2011
Bump!
Does anyone have an idea how to present this kind of "typed value" to the user?
And shouldn't ComObjValue() at least return a string pointer for VT_BSTR?
Join the discussion on The future of AutoHotkey
Posted Image Visit me on github Posted Image
Win7 HP SP1 64bit | AHK_L U 64bit

Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006

ComObjValue(ComObjParameter(0x4000|0xC, arr_data))

You may as well use arr_data directly. ;)

The meaning of "ref" in the context of ComVar is "reference to Variant". It is encapsulated as an object so that you can pass it directly to a COM method. In this case, you're storing a pointer to the variant, so "ref" has lost its meaning.

DllCall("oleaut32\SafeArrayUnaccessData", "ptr", ComObjValue(array))

According to the documentation, this decrements the lock count of the array and invalidates the pointer retrieved by SafeArrayAccessData (arr_data). In reality the data pointer (which can also be retrieved via NumGet) seems to be always valid, but it would be best to follow the documentation - and not call SafeArrayUnaccessData until you're finished with arr_data (var.ref).

COMStruct(ComObjParameter(VT_UINT := 0xB, 1000)) ; yields vt = 0xB - expected

Yielding 0xB is expected. However, 0xB is VT_BOOL.

I have a VARIANT in memory and want to present its value to the AHK user

Construct a SafeArray as you did before, copy the VARIANT into arr_data and use array[0] to retrieve the value. Alternatively, you could construct a SafeArray manually using the original VARIANT as the array's data, wrap the SafeArray pointer with ComObjParameter and then retrieve the value.

val := NumGet(struct, 08, "UInt64") ; is it important to change the type here or does "UInt64" fit all types?

UInt64 isn't supported. NumGet sees the "I" and the "6" and assumes it is Int64. The value you will get is probably equivalent to ComObjValue: a pointer or integer value. If it is a floating-point number, you will get the integer with equivalent binary value. Note that if NumGet did support UInt64, the only way it could support unsigned values larger than 0x7fffffffffffffff would be to return a numeric string.

And shouldn't ComObjValue() at least return a string pointer for VT_BSTR?

ComObjValue is mostly intended for retrieving the value of a COM object wrapper returned by a COM method. Since VT_BSTR is returned as a simple string, there was no need for ComObjValue to support it specifically. Similarly, VT_R8 and VT_R4 are returned as floating-point numbers, while some other types (possibly including VT_DECIMAL) are converted to strings. The remaining types are VT_DISPATCH, VT_UNKNOWN, any with the VT_BYREF or VT_ARRAY flags set, or any unhandled type which VariantChangeType couldn't convert to a string. So basically, for its intended purpose, ComObjValue needed only to return a pointer.

For VT_BSTR you can use StrGet, but of course that's not ideal if the user is calling ComObjValue directly. For floating-point types you would need to reinterpret the integer as a "float" or "double"; you can do this using NumPut and NumGet. However, it's probably better to not use wrapper objects for those types in the first place.

If you wish to keep type information even for simple types such as VT_I4 and VT_BSTR, I would suggest you create your own wrapper object. For simple value types, a SafeArray can be used to retrieve the value. NumGet can be used to retrieve the type.

maul.esel
  • Members
  • 790 posts
  • Last active: Jan 05 2013 09:26 PM
  • Joined: 28 Feb 2011
Hi :)
I haven't forgotten about this thread, just been experimenting / busy. Once again, thanks a lot for your help.

I reworked the code once again:
class CCFramework
{
	CreateVARIANT(value)
	{
		static VT_VARIANT := 0xC, VT_BYREF := 0x4000
		local array, arr_data, variant

		array := ComObjArray(VT_VARIANT, 1)
		array[0] := value

		DllCall("oleaut32\SafeArrayAccessData", "Ptr", ComObjValue(array), "Ptr*", arr_data)
		variant := CCFramework.AllocateMemory(16) ; uses HeapAlloc to allocate 16 bytes (VarSetCapacity seems not fine as AFAIK the pointer is no longer valid when the method returns (?))
		, CCFramework.CopyMemory(arr_data, variant, 16) ; copy memory before the pointer is invalidated (uses RtlMoveMemory)
		DllCall("oleaut32\SafeArrayUnaccessData", "Ptr", ComObjValue(array))

		return { "ref" : variant, "vt" : NumGet(1*variant, 00, "UShort"), "value" : array[0] }
	}

	CopyMemory(src, dest, size)
	{
		DllCall("RtlMoveMemory", "Ptr", dest, "Ptr", src, "UInt", size)
	}

	AllocateMemory(bytes)
	{
		static HEAP_GENERATE_EXCEPTIONS := 0x00000004, HEAP_ZERO_MEMORY := 0x00000008
		return DllCall("HeapAlloc", "Ptr", CCFramework.heap, "UInt", HEAP_GENERATE_EXCEPTIONS|HEAP_ZERO_MEMORY, "UInt", bytes)
	}

	FreeMemory(buffer)
	{
		return DllCall("HeapFree", "Ptr", CCFramework.heap, "UInt", 0, "Ptr", buffer)
	}
}

; Tests:
var := CCFramework.CreateVARIANT(12)
MsgBox % var.ref "`n" NumGet(1*var.ref, 00, "UShort") " = " var.vt "`n" var.value
CCFramework.FreeMemory(var.ref)

var := CCFramework.CreateVARIANT("abc")
MsgBox % var.ref "`n" NumGet(1*var.ref, 00, "UShort") " = " var.vt "`n" var.value
CCFramework.FreeMemory(var.ref)

var := CCFramework.CreateVARIANT(ComObjParameter(VT_BOOL := 0xB, -1))
MsgBox % var.ref "`n" NumGet(1*var.ref, 00, "UShort") " = " var.vt "`n" var.value
CCFramework.FreeMemory(var.ref)

var := CCFramework.CreateVARIANT(ComObjParameter(VT_UNKNOWN := 0xD, ComObjUnwrap(ComObjCreate("Scripting.Dictionary"))))
MsgBox % var.ref "`n" NumGet(1*var.ref, 00, "UShort") " = " var.vt "`n" (IsObject(var.value) ? "[object]" : var.value)
CCFramework.FreeMemory(var.ref)
My tests work fine for this. Any suggestions?

The only strange thing is that the IUnknown pointer is returned as object. I'll propbably change CreateVariant() to do
return { "ref" : variant, "vt" : NumGet(1*variant, 00, "UShort"), "value" : [color=red]IsObject(array[0]) && NumGet(1*variant, 00, "UShort") == 0xD ? ComObjValue(array[0]) : array[0][/color] }
Do you see any problems with that?
Join the discussion on The future of AutoHotkey
Posted Image Visit me on github Posted Image
Win7 HP SP1 64bit | AHK_L U 64bit

maul.esel
  • Members
  • 790 posts
  • Last active: Jan 05 2013 09:26 PM
  • Joined: 28 Feb 2011
And one more question :)
Would this handling also work for PROPVARIANT structures?
Join the discussion on The future of AutoHotkey
Posted Image Visit me on github Posted Image
Win7 HP SP1 64bit | AHK_L U 64bit

Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006

The only strange thing is that the IUnknown pointer is returned as object.

That is for two reasons:
[*:24324zbc]So that the pointer is automatically released when appropriate.
[*:24324zbc]So that the script can know what type of value was returned (i.e. not just an integer).

return { "ref" : variant, "vt" : NumGet(1*variant, 00, "UShort"), "value" : [color=red]IsObject(array[0]) && NumGet(1*variant, 00, "UShort") == 0xD ? ComObjValue(array[0]) : array[0][/color] }
Do you see any problems with that?

array[0] creates a new wrapper object each time it is evaluated. Why bother? You can use NumGet to check the type first, then another NumGet to retrieve the pointer directly rather than creating wrapper objects.

Would this handling also work for PROPVARIANT structures?

I believe PROPVARIANT uses some types that VARIANT does not, so it might not be sufficient.

maul.esel
  • Members
  • 790 posts
  • Last active: Jan 05 2013 09:26 PM
  • Joined: 28 Feb 2011

array[0] creates a new wrapper object each time it is evaluated. Why bother? You can use NumGet to check the type first, then another NumGet to retrieve the pointer directly rather than creating wrapper objects.

Thanks, I changed it:
return { "ref" : variant, "vt" : NumGet(1*variant, 00, "UShort"), "value" : NumGet(1*variant, 00, "UShort") == VT_UNKNOWN ? NumGet(1*variant, 08, "Ptr") : array[0] }

I believe PROPVARIANT uses some types that VARIANT does not, so it might not be sufficient.

That would have been to good to be true :)
Join the discussion on The future of AutoHotkey
Posted Image Visit me on github Posted Image
Win7 HP SP1 64bit | AHK_L U 64bit

maul.esel
  • Members
  • 790 posts
  • Last active: Jan 05 2013 09:26 PM
  • Joined: 28 Feb 2011
Sorry for reviving this thread, just wana know whether this statement:

A VARIANT structure is always exactly 16 bytes.

is also true on 64bit where I'd say the below
struct __tagBRECORD {
   PVOID       pvRecord;
   IRecordInfo *pRecInfo;
} __VARIANT_NAME_4;
is 16 bytes on itself...

So should the size of a VARIANT actually be 08 + 2 * A_PtrSize or am I missing something?
Join the discussion on The future of AutoHotkey
Posted Image Visit me on github Posted Image
Win7 HP SP1 64bit | AHK_L U 64bit

Lexikos
  • Administrators
  • 9844 posts
  • AutoHotkey Foundation
  • Last active:
  • Joined: 17 Oct 2006
You may be right. You could always confirm by checking sizeof(VARIANT) in VC++.

maul.esel
  • Members
  • 790 posts
  • Last active: Jan 05 2013 09:26 PM
  • Joined: 28 Feb 2011
I know but I couldn't get my VC++ Express to compile for 64bit :x That stupid thing!
    Edit: Got it working, and it's indeed 24 bytes on 64bit.
Join the discussion on The future of AutoHotkey
Posted Image Visit me on github Posted Image
Win7 HP SP1 64bit | AHK_L U 64bit