v2 Acc.ahk library Acc_Children() error

Get help with using AutoHotkey (v2 or newer) and its commands and hotkeys
neogna2
Posts: 586
Joined: 15 Sep 2016, 15:44

v2 Acc.ahk library Acc_Children() error

Post by neogna2 » 18 Aug 2022, 02:12

Background: The v1 Acc.ahk library can interact with windows that are otherwise hard to control. Acc.ahk uses Microsoft Active Accessibility (MSAA). Lots of useful v1 scripts require Acc. There is work in progress on v2 Acc.ahk, but some issues remain and we're stuck.

Please help troubleshoot and solve the problem that v2 Acc's Acc_Children() errors in some cases where v1 does not.

Below are two small test scripts to reproduce the issue. Each script opens a File Explorer window and uses Acc to read role texts.

For the v2 script use Acc v2 version 2022-02-22 by eugenesv (this is also the most recent v2 Acc.ahk version)
viewtopic.php?p=433516#p433516

For the v1 script Acc v1 from
https://github.com/Ixiko/AHK-libs-and-classes-collection/blob/master/libs/a-f/ACC.ahk

v2 code (errors)

Code: Select all

#Include Acc.ahk2
Run "C:\"	     ; open File Explorer to C:\ for Acc test
Sleep(500)
If !WinActive("C:\ ahk_class CabinetWClass")
    ExitApp
A := Acc_ObjectFromWindow(hWnd := WinExist("A"), 0x0)  ; get root Acc object
MsgBox Acc_GetRoleText(A.accRole(0))      ; "window"
C := Acc_Children(A)                                 ; get level1 children
MsgBox Acc_GetRoleText(C[1].accRole(0))   ; "menu bar"
CC := Acc_Children(C[1])                            ; (fail to) get level2 children of level1 node1
; "Error in #include Acc.ahk2", "An exception was thrown.", "Specifically: 0xc0000005"
v1 code (working)

Code: Select all

#Include Acc.ahk
Run C:\	     ; open File Explorer to C:\ for Acc test
Sleep 500
If !WinActive("C:\ ahk_class CabinetWClass")
    ExitApp
A := Acc_ObjectFromWindow(hWnd := WinExist("A"), 0x0)     ; get root Acc object
MsgBox % Acc_GetRoleText(A.accRole(0))      ; "window"
C := Acc_Children(A)                                    ; get level1 children
MsgBox % Acc_GetRoleText(C[1].accRole(0))   ; "menu bar"
CC := Acc_Children(C[1])                               ; get level2 children of level1 node1
MsgBox % Acc_GetRoleText(CC[1].accRole(0))   ; "menu item"
(Note that File Explorer is just a simple example here, the same issue happens in other apps too.)

lexikos
Posts: 9560
Joined: 30 Sep 2013, 04:07
Contact:

Re: v2 Acc.ahk library Acc_Children() error

Post by lexikos » 18 Aug 2022, 20:35

0xC0000005 (access violation) usually indicates an error with calculation of address/offsets, or a reference counting error. The line at which the crash occurs can be misleading, as the real error can effectively corrupt the state of the program, seemingly throwing logic out the window.

Your script just exits on my system, as the title of the window is "Local Disk (C:)".

Unrelated to your issue, instead of

Code: Select all

Sleep(500)
If !WinActive("C:\ ahk_class CabinetWClass")
    ExitApp
A := Acc_ObjectFromWindow(hWnd := WinExist("A"), 0x0)  ; get root Acc object
I would suggest

Code: Select all

if !hWnd := WinWait("C: ahk_class CabinetWClass",, 0.5)
    ExitApp
A := Acc_ObjectFromWindow(hWnd, 0x0)  ; get root Acc object
It is difficult to debug interactively when the script requires a window other than the debugger to be active.


In any case, I was unable to produce any crashes. I tested it on v2.0-beta.7, both 32-bit and 64-bit.


One thing that stood out so far is

Code: Select all

Acc_Query(Acc) {
  global IID
  try {
    pIAcc    	:= ComObjQuery(Acc, IID.IAccessible)
    retComObj	:= ComValue(com.DISPATCH, pIAcc, 1)
    return retComObj
  }
}
  • pIAcc implies the author thought it would be a pointer. It is not.
  • I do not recall allowing for objects in the second parameter.
The current documentation is very vague about what values are acceptable for the second parameter. For non-integer values, it only indicates that either "the value is converted to the target type" or "An exception is thrown". The conversion code shares implementation with COM methods, which allow passing a ComValue to specify the type of a parameter. In other words, it is possible to take a ComValue of one variant type, and use ComValue() to convert it to another variant type. In short, it appears this code will work as intended.

Note that the third parameter of ComValue has no effect for VT_DISPATCH in v2. When passed a pointer value (not a ComValue), ComValue and ComObjFromPtr always take ownership of it.

lexikos
Posts: 9560
Joined: 30 Sep 2013, 04:07
Contact:

Re: v2 Acc.ahk library Acc_Children() error

Post by lexikos » 18 Aug 2022, 21:52

See if replacing AccChild := Acc_Query(child) with AccChild := ComValue(9, child) (and removing ObjRelease(child)) makes any difference. I see no reason to query for IAccessible, especially since ComValue(com.DISPATCH, pIAcc, 1) implicitly queries for IDispatch (and is therefore not guaranteed to return a wrapped IAccessible interface pointer). If it's even possible for a child object to not implement IAccessible, you probably don't want to push an empty string in that case (and that's what would happen if you use Acc_Query()).

neogna2
Posts: 586
Joined: 15 Sep 2016, 15:44

Re: v2 Acc.ahk library Acc_Children() error

Post by neogna2 » 19 Aug 2022, 17:42

Thanks @Lexikos. It will take my brain some time to work through your feedback and suggestions, both here and in the other thread. In the meantime I'm pinging in @ludamo and @eugenesv who each started one the v2 Acc work in progress ports.
Unrelated to your issue, instead of ... I would suggest ...
Yeah, thanks. My script mistakenly assumed some non-default File Explorer settings.
In any case, I was unable to produce any crashes.
Hm, now that v2 test script doesn't throw error for me either with v2 Acc from 2022-02-22. I'm sitting here quite confused which earlier v2 Acc version I must have used run the tests and got the errors with before I started this thread :crazy: I'll try to track that down.

In the meantime here's another pair of v1 vs v2 scripts that uses a Notepad++ window.
I tested now in Windows 10 with AutoHotkey v2.beta7 x64, v2 Acc from 2022-02-22 and the latest portable Notepad++ v4.8.8.
The v2 script throws error 0xc0000005 at the v2 Acc line in Acc_Children() that start with with gotChildren := DllCall("oleacc\AccessibleChildren" .... But the v1 scripts works.

v2 (error)

Code: Select all

#Include Acc.ahk2
if !hWnd := WinExist("ahk_exe notepad++.exe")
    ExitApp
A := Acc_ObjectFromWindow(hWnd, 0x0)  ; root
A := Acc_Children(A)
A := Acc_Children(A[4])       ;client  4
A := Acc_Children(A[18])      ;window 4.18
A := Acc_Children(A[4])       ;client 4.18.4
;throws error on next line
A := Acc_Children(A[1])    ; window 4.18.4.1
;never reaches this line
MsgBox Acc_GetRoleText(A[1].accRole(0)) ;menu bar 4.18.4.1.1
v1 (no error)

Code: Select all

#Include Acc.ahk
if !hWnd := WinExist("ahk_exe notepad++.exe")
    ExitApp
A := Acc_ObjectFromWindow(hWnd, 0x0)  ; root
A := Acc_Children(A)
A := Acc_Children(A[4])       ;client  4
A := Acc_Children(A[18])      ;window 4.18
A := Acc_Children(A[4])       ;client 4.18.4
A := Acc_Children(A[1])       ;window 4.18.4.1
MsgBox % Acc_GetRoleText(A[1].accRole(0)) ;menu bar 4.18.4.1.1
eugenesv also posted a more generic v2 test script that takes a whole Acc path as input.
We can list all Acc paths from a window with jeeswg's v1 Acc script JEE_AccGetTextAll.

Descolada
Posts: 1101
Joined: 23 Dec 2021, 02:30

Re: v2 Acc.ahk library Acc_Children() error

Post by Descolada » 20 Aug 2022, 07:18

@neogna2, as @lexikos suggested the cause of the problem is Acc_Query.
Changing this part (or something equivalent)

Code: Select all

Acc_Query(Acc) {
  global IID
  try {
    pIAcc    	:= ComObjQuery(Acc, IID.IAccessible)
    retComObj	:= ComValue(com.DISPATCH, pIAcc, 1)
    return retComObj
  }
}
to this

Code: Select all

Acc_Query(Acc) {
  global IID
  try {
    pIAcc    	:= ComObjQuery(Acc, IID.IAccessible)
    ObjAddRef(pIAcc.ptr)
    retComObj	:= ComValue(com.DISPATCH, pIAcc.ptr)
    return retComObj
  }
}
fixed the crash. I am uncertain whether pIAcc.ptr needs to be released at a later time - most likely not.

@lexikos, perhaps it would be clearer to change the documentation for ComObjQuery from InterfacePointer := ComObjQuery(ComObj, SID, IID) to something like InterfaceComObj := ComObjQuery(ComObj, SID, IID) to make it clear that a pointer isn't being returned.
Also, could ComValue() perhaps be modified to accept a ComValue object and handle the ObjAddRef? Or at least throw a TypeError if an object shouldn't be provided...
Btw, AccChild := ComValue(9, child) didn't work, results in the same error.

neogna2
Posts: 586
Joined: 15 Sep 2016, 15:44

Re: v2 Acc.ahk library Acc_Children() error

Post by neogna2 » 20 Aug 2022, 14:46

Descolada wrote:
20 Aug 2022, 07:18
the cause of the problem is Acc_Query. Changing ... to this

Code: Select all

Acc_Query(Acc) {
  global IID
  try {
    pIAcc    	:= ComObjQuery(Acc, IID.IAccessible)
    ObjAddRef(pIAcc.ptr)
    retComObj	:= ComValue(com.DISPATCH, pIAcc.ptr)
    return retComObj
  }
}
fixed the crash.
Hi Descolada, I tried that change but still get the crash in the notepad++ test script.
Update: wait I also changed back to AccChild := Acc_Query(child) and now the notepad++ test script works. Yay, progress! :)
I will investigate if that change solves also other problem cases and also look into the other feedback from Lexikos and report back my findings here.

lexikos
Posts: 9560
Joined: 30 Sep 2013, 04:07
Contact:

Re: v2 Acc.ahk library Acc_Children() error

Post by lexikos » 20 Aug 2022, 22:29

@lexikos, perhaps it would be clearer to change the documentation for ComObjQuery from InterfacePointer := ComObjQuery(ComObj, SID, IID) to something like InterfaceComObj := ComObjQuery(ComObj, SID, IID) to make it clear that a pointer isn't being returned.
I already updated the return value section to reflect the fact that passing IID_IDispatch affects the variant type of the return value (the same as with ComObject(clsid, iid)), but overlooked that variable name. Thanks.
Also, could ComValue() perhaps be modified to accept a ComValue object and handle the ObjAddRef?
It already does, as I mentioned above.
lexikos wrote:
18 Aug 2022, 20:35
The conversion code shares implementation with COM methods, which allow passing a ComValue to specify the type of a parameter. In other words, it is possible to take a ComValue of one variant type, and use ComValue() to convert it to another variant type.
I've already updated the documentation to clarify this.
Value
The value to wrap.
If this is a pure integer and VarType is not VT_R4, VT_R8, VT_DATE or VT_CY, its value is used directly; in particular, VT_BSTR, VT_DISPATCH and VT_UNKNOWN can be initialized with a pointer value.
In any other case, the value is copied into a temporary VARIANT using the same rules as normal COM methods calls. If the source variant type is not equal to VarType, conversion is attempted by calling VariantChangeType with a wFlags value of 0. An exception is thrown if conversion fails.
...
Conversion from VT_UNKNOWN to VT_DISPATCH results in a call to IUnknown::QueryInterface, which may produce an interface pointer different to the original, and will throw an exception if the object does not implement IDispatch. By contrast, if Value is an integer and VarType is VT_DISPATCH, the value is used directly, and therefore must be an IDispatch-compatible interface pointer.
Source: ComValue - Syntax & Usage | AutoHotkey v2
In my debugging prior to writing this documentation, it appeared that ComValue(9,...) was returning an object that wrapped the same interface pointer, already known to be IAccessible*. Just now, I took the original broken code and replaced "Ptr", AccPtr with "Ptr", ComObjQuery(Acc, IID.IAccessible). This fixed the crashing, which indicates that AccPtr was not IAccessible*. Further debugging revealed that although most of the children were already IAccessible*, there are some for which ComObjQuery returns a different pointer, and then ComValue(9,...) returns the original pointer.
Btw, AccChild := ComValue(9, child) didn't work, results in the same error.
That's because the children are only guaranteed to be IDispatch*, which is only sometimes (perhaps usually) the same address as the object's IAccessible*. Since child is an integer, ComValue does not call QueryInterface.
if (ComObjType(Acc,"Name") != "IAccessible") {
This needs to be replaced. Note what "Name" returns:
The name of the object's default interface.
Source: ComObjType - Syntax & Usage | AutoHotkey v2
The correct way to determine that you have an IAccessible* is to call QueryInterface, a.k.a. ComObjQuery. You can do that inline in DllCall's parameters, as I showed above.
NewError := Error("" , -1)
I'm not sure what purpose this was supposed to have, but it appears to be overwriting the previously assigned Error with one that has no message. I think that should be removed, and NewError.Message should be added to the MsgBox call so that the actual error is displayed...
global IID
You don't need this anymore (you're already using the global object com without declaration, so may as well remove this one).

lexikos
Posts: 9560
Joined: 30 Sep 2013, 04:07
Contact:

Re: v2 Acc.ahk library Acc_Children() error

Post by lexikos » 23 Aug 2022, 03:56

If you replace the type check with ComObjQuery(Acc, IID.IAccessible) and pass the result to AccessibleChildren or any other function that needs IAccessible*, then you won't need Acc_Children() to query each child object before returning. In that case, you can take a shortcut with the VARIANT array. I used code like this during previous testing (within Acc_Children()):

Code: Select all

    avChildren := ComObjArray(0xC, cChildren)
    pChildren := NumGet(avChildren.ptr, 8+A_PtrSize, "ptr")  ; SAFEARRAY::pvData
    gotChildren := DllCall("oleacc\AccessibleChildren"
      , "Ptr" 	, ComObjQuery(Acc, IID.IAccessible)
      , "Int" 	, 0             	; LONG       	 iChildStart
      , "Int" 	, cChildren     	; LONG       	 cChildren
      , "Ptr" 	, pChildren     	; VARIANT    	*rgvarChildren
      , "Int*"	, &retCountCh:=0	; LONG       	*pcObtained
      )
    if (gotChildren = 0) {
      return [avChildren*]  ; Create an Array from the SAFEARRAY
    }
Technically it seems you're supposed to use SafeArrayAccessData/UnaccessData to get the pointer, but I've never found it to be necessary.

Also, I suppose that this might misbehave if retCountCh is different to cChildren.

Post Reply

Return to “Ask for Help (v2)”