RegisterSyncCallback (for multi-threaded APIs)

Post your working scripts, libraries and tools
lexikos
Posts: 6207
Joined: 30 Sep 2013, 04:07
GitHub: Lexikos

RegisterSyncCallback (for multi-threaded APIs)

06 Aug 2016, 21:37

Code: Select all

/*
    RegisterSyncCallback
    
    A replacement for RegisterCallback for use with APIs that will call
    the callback on the wrong thread.  Synchronizes with the script's main
    thread via a window message.
    
    This version tries to emulate RegisterCallback as much as possible
    without using RegisterCallback, so shares most of its limitations,
    and some enhancements that could be made are not.
    
    Other differences from v1 RegisterCallback:
      - Variadic mode can't be emulated exactly, so is not supported.
      - A_EventInfo can't be set in v1, so is not supported.
      - Fast mode is not supported (the option is ignored).
      - ByRef parameters are allowed (but ByRef is ignored).
      - Throws instead of returning "" on failure.
*/
RegisterSyncCallback(FunctionName, Options:="", ParamCount:="")
{
    if !(fn := Func(FunctionName)) || fn.IsBuiltIn
        throw Exception("Bad function", -1, FunctionName)
    if (ParamCount == "")
        ParamCount := fn.MinParams
    if (ParamCount > fn.MaxParams && !fn.IsVariadic || ParamCount+0 < fn.MinParams)
        throw Exception("Bad param count", -1, ParamCount)
    
    static sHwnd := 0, sMsg, sSendMessageW
    if !sHwnd
    {
        Gui RegisterSyncCallback: +Parent%A_ScriptHwnd% +hwndsHwnd
        OnMessage(sMsg := 0x8000, Func("RegisterSyncCallback_Msg"))
        sSendMessageW := DllCall("GetProcAddress", "ptr", DllCall("GetModuleHandle", "str", "user32.dll", "ptr"), "astr", "SendMessageW", "ptr")
    }
    
    if !(pcb := DllCall("GlobalAlloc", "uint", 0, "ptr", 96, "ptr"))
        throw
    DllCall("VirtualProtect", "ptr", pcb, "ptr", 96, "uint", 0x40, "uint*", 0)
    
    p := pcb
    if (A_PtrSize = 8)
    {
        /*
        48 89 4c 24 08  ; mov [rsp+8], rcx
        48 89 54'24 10  ; mov [rsp+16], rdx
        4c 89 44 24 18  ; mov [rsp+24], r8
        4c'89 4c 24 20  ; mov [rsp+32], r9
        48 83 ec 28'    ; sub rsp, 40
        4c 8d 44 24 30  ; lea r8, [rsp+48]  (arg 3, &params)
        49 b9 ..        ; mov r9, .. (arg 4, operand to follow)
        */
        p := NumPut(0x54894808244c8948, p+0)
        p := NumPut(0x4c182444894c1024, p+0)
        p := NumPut(0x28ec834820244c89, p+0)
        p := NumPut(  0xb9493024448d4c, p+0) - 1
        lParamPtr := p, p += 8
        
        p := NumPut(0xba, p+0, "char") ; mov edx, nmsg
        p := NumPut(sMsg, p+0, "int")
        p := NumPut(0xb9, p+0, "char") ; mov ecx, hwnd
        p := NumPut(sHwnd, p+0, "int")
        p := NumPut(0xb848, p+0, "short") ; mov rax, SendMessageW
        p := NumPut(sSendMessageW, p+0)
        /*
        ff d0        ; call rax
        48 83 c4 28  ; add rsp, 40
        c3           ; ret
        */
        p := NumPut(0x00c328c48348d0ff, p+0)
    }
    else ;(A_PtrSize = 4)
    {
        p := NumPut(0x68, p+0, "char")      ; push ... (lParam data)
        lParamPtr := p, p += 4
        p := NumPut(0x0824448d, p+0, "int") ; lea eax, [esp+8]
        p := NumPut(0x50, p+0, "char")      ; push eax
        p := NumPut(0x68, p+0, "char")      ; push nmsg
        p := NumPut(sMsg, p+0, "int")
        p := NumPut(0x68, p+0, "char")      ; push hwnd
        p := NumPut(sHwnd, p+0, "int")
        p := NumPut(0xb8, p+0, "char")      ; mov eax, &SendMessageW
        p := NumPut(sSendMessageW, p+0, "int")
        p := NumPut(0xd0ff, p+0, "short")   ; call eax
        p := NumPut(0xc2, p+0, "char")      ; ret argsize
        p := NumPut((InStr(Options, "C") ? 0 : ParamCount*4), p+0, "short")
    }
    NumPut(p, lParamPtr+0) ; To be passed as lParam.
    p := NumPut(&fn, p+0)
    p := NumPut(ParamCount, p+0, "int")
    return pcb
}

RegisterSyncCallback_Msg(wParam, lParam)
{
    if (A_Gui != "RegisterSyncCallback")
        return
    fn := Object(NumGet(lParam + 0))
    paramCount := NumGet(lParam + A_PtrSize, "int")
    params := []
    Loop % paramCount
        params.Push(NumGet(wParam + A_PtrSize * (A_Index-1)))
    return %fn%(params*)
}
AutoHotkey does not support real multi-threading. If you try to run code on a second thread, a number of things can happen:
  • The second thread might hang.
  • The process might crash.
  • One or the other thread might do very strange things, like passing parameters calculated on one thread to commands being called on the other thread.
  • Memory might be corrupted, such that the process doesn't crash but other unpredictable things happen.
In short, it's a very bad idea.

Sometimes, an API might ask for a pointer to a callback function, and might call that function from a worker thread. If you let it do that, the script will be unreliable. RegisterSyncCallback provides a limited solution to the problem by creating a callback which does not call the script's function directly, but instead synchronises with the script's main thread by sending a window message.

The following artificial example shows some of what can happen when code is run on the wrong thread. Instead of using an API which calls the callback from a worker thread, it just creates a new (real) thread.

Code: Select all

MsgBox % "Main thread ID: " DllCall("GetCurrentThreadId")

cb := RegisterCallback("MyFn")
; cb := RegisterSyncCallback("MyFn")

; DllCall(cb, "ptr", 123)  ; Works OK.

DllCall("CreateThread", "ptr", 0, "ptr", 0, "ptr", cb, "ptr", 456, "uint", 0, "uint*", 0)

t := A_TickCount
while (A_TickCount-t < 1000) {
    ; do stuff
    if some_var
        break
}
MsgBox
ExitApp

MyFn(arg) {
    MsgBox 0, MyFn, % arg "`nThread ID: " DllCall("GetCurrentThreadId")
}
On my system, the MyFn MsgBox is usually blank and shows OK/Cancel instead of just OK. If the script is changed slightly, it might crash instead.

Replacing RegisterCallback with RegisterSyncCallback fixes the instability by forcing the function to run on the right thread. Of course, this does not allow one to use real multi-threading, and is not meant to.
just me
Posts: 5841
Joined: 02 Oct 2013, 08:51
Location: Germany

Re: RegisterSyncCallback (for multi-threaded APIs)

07 Aug 2016, 10:53

Thanks, it is working stable with IAutoComplete/IEnumStrings as yet. Any chance to get it built-in?
gwarble
Posts: 321
Joined: 30 Sep 2013, 15:01

Re: RegisterSyncCallback (for multi-threaded APIs)

07 Aug 2016, 13:35

awesome, thanks for writing that!
EitherMouse - Multiple mice, individual settings . . . . www.EitherMouse.com . . . . forum . . . .
gwarble
Posts: 321
Joined: 30 Sep 2013, 15:01

Re: RegisterSyncCallback (for multi-threaded APIs)

08 Aug 2016, 15:19

works great in my iAutoComplete2 test scripts when ran uncompiled, but fails when compiled

any idea why?

thanks
- joel
EitherMouse - Multiple mice, individual settings . . . . www.EitherMouse.com . . . . forum . . . .
lexikos
Posts: 6207
Joined: 30 Sep 2013, 04:07
GitHub: Lexikos

Re: RegisterSyncCallback (for multi-threaded APIs)

08 Aug 2016, 20:11

No. Are you compiling with the same version of AutoHotkey?
gwarble
Posts: 321
Joined: 30 Sep 2013, 15:01

Re: RegisterSyncCallback (for multi-threaded APIs)

08 Aug 2016, 21:52

oh yeah you're right i was running 1.1.23.01 uncompiled and 1.1.16.05 compiled, oops
thanks
EitherMouse - Multiple mice, individual settings . . . . www.EitherMouse.com . . . . forum . . . .
arcticir
Posts: 544
Joined: 17 Nov 2013, 11:32

Re: RegisterSyncCallback (for multi-threaded APIs)

02 Apr 2017, 22:00

What caused this difference?

Image

Code: Select all

test:=0
EnumAddress := RegisterSyncCallback("EnumWindowsProc", "Fast")
DetectHiddenWindows On
DllCall("EnumWindows", Ptr, EnumAddress, Ptr, 0)
Sync:=test,test:=0
EnumAddress := RegisterCallback("EnumWindowsProc", "Fast")
DllCall("EnumWindows", Ptr, EnumAddress, Ptr, 0)
MsgBox % "RegisterCallback: " test "`nRegisterSyncCallback " sync
    
EnumWindowsProc(hwnd, lParam)
{
    global Output,test
    WinGetTitle, title, ahk_id %hwnd%
    WinGetClass, class, ahk_id %hwnd%
    if title
        test++
    return true  ; Tell EnumWindows() to continue until all windows have been enumerated.
}
lexikos
Posts: 6207
Joined: 30 Sep 2013, 04:07
GitHub: Lexikos

Re: RegisterSyncCallback (for multi-threaded APIs)

02 Apr 2017, 22:15

DetectHiddenWindows, and this.
Fast mode is not supported (the option is ignored).
arcticir
Posts: 544
Joined: 17 Nov 2013, 11:32

Re: RegisterSyncCallback (for multi-threaded APIs)

02 Apr 2017, 23:51

This script does not use fast mode and will not receive properly. thanks.

Code: Select all

EnumAddress := RegisterSyncCallback("EnumWindowsProc")
lexikos
Posts: 6207
Joined: 30 Sep 2013, 04:07
GitHub: Lexikos

Re: RegisterSyncCallback (for multi-threaded APIs)

03 Apr 2017, 01:26

Firstly, "This script" registers a callback and then does absolutely nothing with it.

Secondly, the Fast option is ignored, so obviously removing it is going to have no effect.

You need to set DetectHiddenWindows correctly. If you do that, the callback will return the same number regardless of whether RegisterCallback or RegisterSyncCallback was used.

Or you could remove "Fast" from both callbacks to get consistent results.

Return to “Scripts and Functions”

Who is online

Users browsing this forum: pkip, Relayer and 25 guests