COM Object Introspection

Get help with using AutoHotkey and its commands and hotkeys
[Shambles]
Posts: 91
Joined: 20 May 2014, 21:24

COM Object Introspection

18 Nov 2014, 23:44

I would like to add support for COM objects to Plaster Introspection Library's HasProperty and HasMethod.

I found some relevant code by jethrow here: Enumerate Com Object Members - iTypeInfo

chaidy correctly observes it does not work for all scriptable COM objects. Shell.Application is one such example.

Not all COM objects are scriptable or introspectable, but Shell.Application is both. This can be observed with the following PowerShell code:

Code: Select all

$shell = new-object -com shell.application
$shell | get-member
I attempted to solve the problem myself. None of Shell.Application's members' names can be read, and are thus filtered out by the line with the comment "Exclude Members that didn't return a Name". I have not been able to determine why their names are unreadable.

Fixing this would only partially solve my problem. I also need to be able to read COM's equivalent to AutoHotkey's MinParams, MaxParams, and IsVariadic values.
User avatar
jethrow
Posts: 188
Joined: 30 Sep 2013, 19:52
Location: Iowa

Re: COM Object Introspection

19 Nov 2014, 18:41

You could look into IsMemberOf

Code: Select all

IsMemberOf(obj, name) {
	return, DllCall(NumGet(NumGet(1*p:=ComObjValue(obj))+A_PtrSize*5), "Ptr",p, "Ptr",VarSetCapacity(iid,16,0)*0+&iid, "Ptr*",&name, "UInt",1, "UInt",1024, "Int*",dispID)=0 && dispID+1
}
[Shambles]
Posts: 91
Joined: 20 May 2014, 21:24

Re: COM Object Introspection

19 Nov 2014, 20:44

Thanks for referring me to more relevant code.

I am a little unsettled by the low-level tinkering with the vtable, but I suppose that is unavoidable and I will get used to it eventually. I read an explanation of how to discover the right integers to use, and think I understand that part.

I searched for a win32 API function or method named IsMemberOf, but could not find anything relevant. What exactly is that DllCall calling?

Do you have any idea how I might get the information equivalent to MinParams, MaxParams, and IsVariadic?
User avatar
jethrow
Posts: 188
Joined: 30 Sep 2013, 19:52
Location: Iowa

Re: COM Object Introspection

22 Nov 2014, 22:08

[Shambles] wrote:I am a little unsettled by the low-level tinkering with the vtable, but I suppose that is unavoidable and I will get used to it eventually ... I searched for a win32 API function or method named IsMemberOf, but could not find anything relevant. What exactly is that DllCall calling?
Join the club - low-level com-interface code isn't exactly a strong point for most coders in this community. IsMemberOf is not a member of the Com-Object. The function is calling the 6th member of the vTable (GetFuncDesc GetIDsOfNames I believe). The most reliable place to find members of the vTable is look in the header file where the com-object vTable is defined.

This might lead you to useful information:
iTypeInfo - Get info from TypeAttr Struct - Enum Com Members
Coco
Posts: 771
Joined: 29 Sep 2013, 20:37
GitHub: cocobelgica

Re: COM Object Introspection

23 Nov 2014, 04:31

jethrow wrote:The function is calling the 6th member of the vTable (GetFuncDesc I believe).
I'm not so sure but if my understanding is correct, it's calling the GetIDsOfNames method of the IDispatch interface. I reformatted jethrow's function for [Shambles]' reference:

Code: Select all

IsMemberOf(obj, name)
{
	pDisp := ComObjValue(obj)
	GetIDsOfNames := NumGet(NumGet(pDisp + 0), 5*A_PtrSize)
	VarSetCapacity(IID_NULL, 16, 0), DISPID := 0 ;// Make #Warn happy
	r := DllCall(GetIDsOfNames, "Ptr", pDisp, "Ptr", &IID_NULL, "Ptr*", &name, "UInt", 1, "UInt", 1024, "Int*", DISPID)
	return (r == 0) && (DISPID + 1)
}
Now here's the definition of GetIDsOfNames method from MSDN:

Code: Select all

HRESULT GetIDsOfNames(
  [in]                    REFIID riid,
  [in, size_is(cNames)]   LPOLESTR *rgszNames,
  [in]                    UINT cNames,
  [in]                    LCID lcid,
  [out, size_is(cNames)]  DISPID *rgDispId
);
And here's the DllCall:

Code: Select all

DllCall(GetIDsOfNames     ; IDispatch::GetIDsOfNames
      , "Ptr",  pDisp     ; pointer to IDisptach
      , "Ptr",  &IID_NULL ; [in] REFIID riid
      , "Ptr*", &name     ; LPOLESTR *rgszNames
      , "UInt", 1         ; UINT cNames
      , "UInt", 1024      ; LCID lcid
      , "Int*", DISPID)   ; DISPID *rgDispId
User avatar
jethrow
Posts: 188
Joined: 30 Sep 2013, 19:52
Location: Iowa

Re: COM Object Introspection

23 Nov 2014, 10:49

My bad - GetFuncDesc is the 6th member of the ITypeInfo Interface.
[Shambles]
Posts: 91
Joined: 20 May 2014, 21:24

Re: COM Object Introspection

24 Nov 2014, 07:12

Thank you jethrow and Coco for taking time to help me with this. It is not something I think I could figure out on my own.

It looks like the header file I need to refer to is oaidl.h. Are there any others I will need? I have needed winuser.h for some constants (e.g. to use with SendMessage) in the past.

I am still having a hard time understanding most of IsMemberOf.

The nested NumGets, ComObjValue, and pointer arithmetic are for retrieving the address of the method to call? First get the address of the COM object, then get its vtable, then get the method's address?

1*p would normally be equivalent to just p. Is this used to force a value into what I have heard called "pure numeric" format?

Is it safe to assume the write to p occurs before the read in the same call? Is the order of evaluation of arguments in AutoHotkey always from left to right, as in most high-level programming languages that define it, instead of right to left as it often is in unoptimized C and C++, or unpredictable as it often is in optimized C and C++?

This part confuses me VarSetCapacity(iid,16,0)*0+&iid.

I am sure VarSetCapacity is being used to reserve space. I guess that is for an array of names, based on the GetIDsOfNames documentation. How does a single name become an array in this case?

Where does iid come from?

Why a 16 for the capacity? How do we know that will always be enough?

I assume the 0 fill byte is for null termination of the string.

Is the result of VarSetCapacity being multiplied by 0 just to hide its existence in the expression, since it should always result in 0?

Based on Microsoft's documentation I would think the third argument would be the count of names, but it seems to be the name itself here?

Then again, it seems to show 5 arguments, while there are 6 here. I am not counting the pointer to the method.

From trying to compare to Coco's explanation it looks like the extra argument is for the pointer to IDispatch. Is that the implicit "this" argument for the method?

I am guessing the remaining arguments have to do with the locale and allocating space for the "out variable". They are hard coded. How do we know they are always correct?

The documentation says GetIDsOfNames returns S_OK for success, which seems to be defined in winerror.h (success is defined as an error in Windows...) as 0. The =0 was supposed to be ==0? It was changed to that in Coco's version, and I can see the purpose if that was what was intended.

Why is 1 being added to the "out variable" before it is logical anded with the GetIDsOfNames result? I think that would always make it true unless we were working with -1 and 0.

Do I need to worry about doing some sort of cleanup as Lexikos mentions (ObjRelease)?

It seems I need to read about the fields and methods of IDispatch and ITypeInfo. Is there anything else I should read about for this task?

I still do not know why the names for Shell.Application members are null. I have read about typelibs sometimes needing to be loaded. Could that be it? If so, do either of you know how I would correct it? I found a LoadTypeLib function.

Do either of you know how I would get the scriptable COM object method equivalent of MinParams, MaxParams, and IsVariadic? I know scriptable COM object methods have required and optional parameters, with the same restrictions as AutoHotkey. I do not know if they can be variadic.

I saw these comments in jethrow's code (iTypeInfo - Get info from TypeAttr Struct - Enum Com Members)

Code: Select all

; Args := NumGet(FuncDesc+0, 36, "short") ; get Num of Args
; Opt := NumGet(FuncDesc+0, 38, "short") ; get Num of Opt Args
lexikos
Posts: 7085
Joined: 30 Sep 2013, 04:07
GitHub: Lexikos

Re: COM Object Introspection

25 Nov 2014, 03:21

[Shambles] wrote:1*p would normally be equivalent to just p.
If you pass a plain variable reference to NumGet in v1, it uses the variable's address (&var) instead of the address stored within the variable.
Is the order of evaluation of arguments in AutoHotkey always from left to right,
Yes, but it is not guaranteed by the documentation.
This part confuses me VarSetCapacity(iid,16,0)*0+&iid.
It sets iid's capacity to 16 bytes, initializing each byte to zero, and then returns the address of iid's content (&iid). It is not an array; just a variable which contains a memory block of 16 or more zero bytes. It is equivalent to IID_NULL, which is what MSDN states you must always pass for the first parameter (riid).

x*0+y in this case is being used to evaluate x before y, and return y. I wouldn't recommend writing code that way.
Based on Microsoft's documentation I would think the third argument would be the count of names, but it seems to be the name itself here?
Microsoft's documentation is for C++, so the "this" (p) parameter is implied (hidden).
I am guessing the remaining arguments have to do with the locale and allocating space for the "out variable". They are hard coded. How do we know they are always correct?
If the COM server (the code which implements that COM object) supports that locale, it will work. When you use the * suffix, as in "Int*", DllCall takes care of allocating space and copying the value between it and your variable before and after the call. For output parameters, the input value is irrelevant.
The =0 was supposed to be ==0?
In most places in an expression, = is case-insensitive comparison, not assignment. For numerical comparisons, = and == behave identically.
I think that would always make it true unless we were working with -1 and 0.
If dispID is 0, dispID+1 is 1, which is true. You are correct about -1, and that is the point. The expression dispID+1 is always true, unless dispID is -1 or non-numeric.
Do I need to worry about doing some sort of cleanup as Lexikos mentions (ObjRelease)?
You are not incrementing the reference counter, nor is the function, nor is the function returning a new reference to you, so you do not need to decrement any reference counters (by calling ObjRelease).

Return to “Ask For Help”

Who is online

Users browsing this forum: AHKStudent, Albireo, Bing [Bot], Google [Bot], hasantr, Himelco, Spyre and 49 guests