RegisterSyncCallback (for multi-threaded APIs)

Post your working scripts, libraries and tools for AHK v1.1 and older
lexikos
Posts: 9553
Joined: 30 Sep 2013, 04:07
Contact:

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: 9424
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?
User avatar
gwarble
Posts: 524
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 . . . .
User avatar
gwarble
Posts: 524
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: 9553
Joined: 30 Sep 2013, 04:07
Contact:

Re: RegisterSyncCallback (for multi-threaded APIs)

08 Aug 2016, 20:11

No. Are you compiling with the same version of AutoHotkey?
User avatar
gwarble
Posts: 524
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: 693
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: 9553
Joined: 30 Sep 2013, 04:07
Contact:

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: 693
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: 9553
Joined: 30 Sep 2013, 04:07
Contact:

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.
arcticir
Posts: 693
Joined: 17 Nov 2013, 11:32

Re: RegisterSyncCallback (for multi-threaded APIs)

14 Jun 2021, 17:17

@lexikos
Can you check if this function works with 64-bit AHK?
I use this function instead of RegisterCallback, which always causes the GUI to be unresponsive.
In 32-bit AHK, everything works fine with this function, but when I switch to 64-bit, it randomly causes the process to crash. And if I use RegisterCallback instead of it, it becomes normal again.
This function is a good solution to the problem of RegisterCallback causing the GUI to get stuck, is there any chance it will be built into V2? Thanks.
malcev
Posts: 1769
Joined: 12 Aug 2014, 12:37

Re: RegisterSyncCallback (for multi-threaded APIs)

19 Nov 2022, 11:15

@lexikos, do You know how We can use RegisterSyncCallback if during receiving data We call context menu in our gui, like it does RegisterCallback?
As I understand We cannot receive windows messages while WM_ENTERMENULOOP.
lexikos
Posts: 9553
Joined: 30 Sep 2013, 04:07
Contact:

Re: RegisterSyncCallback (for multi-threaded APIs)

19 Nov 2022, 17:26

@malcev you can either create your own window procedure with RegisterCallback() and use that instead of OnMessage, or just block WM_ENTERMENULOOP. See [Solved] OnMessage(WM_INITPOPUPMENU) doesn't work.
malcev
Posts: 1769
Joined: 12 Aug 2014, 12:37

Re: RegisterSyncCallback (for multi-threaded APIs)

19 Nov 2022, 20:56

Thank You. This successfully blocks WM_ENTERMENULOOP in our gui.

Code: Select all

OnMessage(0x211, "WM_ENTERMENULOOP")
WM_ENTERMENULOOP()
{
   Return 100
}
But what We can block when menu from tray is opened?
Also will this code work OK if I replace in RegisterSyncCallback function
this

Code: Select all

OnMessage(sMsg := 0x8000, Func("RegisterSyncCallback_Msg"))
to this

Code: Select all

global pWndProcOld := DllCall("SetWindowLong" (A_PtrSize=8 ? "Ptr" : ""), "uptr", sHwnd, "int", GWL_WNDPROC := -4, "ptr", RegisterCallback(Func("RegisterSyncCallback_Msg")), "ptr")
And RegisterSyncCallback_Msg will look like this

Code: Select all

RegisterSyncCallback_Msg(hWnd, uMsg, wParam, lParam)
{
    global pWndProcOld
    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*) + 0*DllCall("CallWindowProc", "ptr", pWndProcOld, "uptr", hWnd, "uint", uMsg, "uptr", wParam, "ptr", lParam)
}
lexikos
Posts: 9553
Joined: 30 Sep 2013, 04:07
Contact:

Re: RegisterSyncCallback (for multi-threaded APIs)

19 Nov 2022, 22:23

malcev wrote:
19 Nov 2022, 20:56
But what We can block when menu from tray is opened?
I think the only way is to intercept the AHK_NOTIFYICON message and show the tray menu yourself by calling TrackPopupMenuEx.

AutoHotkey v2 does not restrict OnMessage in this way.

Also will this code work OK
If you use it, you will find out.
wyrover
Posts: 3
Joined: 19 May 2015, 20:50

Re: RegisterSyncCallback (for multi-threaded APIs)

04 Dec 2022, 02:59

How to support string parameters?
lexikos
Posts: 9553
Joined: 30 Sep 2013, 04:07
Contact:

Re: RegisterSyncCallback (for multi-threaded APIs)

04 Dec 2022, 03:55

The same way as with RegisterCallback. Use StrGet.
wyrover
Posts: 3
Joined: 19 May 2015, 20:50

Re: RegisterSyncCallback (for multi-threaded APIs)

04 Dec 2022, 05:04

switch to the UI thread callback by responding to the WM_NULL message.

Code: Select all


#Noenv
#persistent

SetBatchLines -1

global hwnd_main := 0
global WM_NULL := 0x0000
global lp_process_ui_callback := RegisterCallback("process_ui_callback")
global lp_recevie_message := RegisterCallback("recevie_message")
global hThread := 0
global hThread2 := 0
global msg
global exit_flag := 0



global message_queue := []

recevie_message() {

  zmq := new ZeroMQ

  ; keypair := zmq.curve_keypair()
  ; MsgBox, % keypair.1 "`n" keypair.2

  context := zmq.context()
  socket := context.socket(zmq.SUB)

  socket.setsockopt_string(zmq.CURVE_SERVERKEY, "HIm0q5eoJ}Ur7-&prX?y{%wiI3L)I8>ge%sE}l/k", "utf-8")
  socket.setsockopt_string(zmq.CURVE_PUBLICKEY, "YHtx0zuc(ZkWoXaozP*JP+Kg<!#Dt.2S>oNUXn?Y", "utf-8")
  socket.setsockopt_string(zmq.CURVE_SECRETKEY, "{r]PaKZ%WD.q$VAk=E5jMeFkMkSep.Lk]voT/db+", "utf-8")

  socket.connect("tcp://192.168.79.129:3002")

  filter := "10001"
  socket.setsockopt_string(zmq.SUBSCRIBE, filter, "utf-8")

  poller := zmq.poller([[socket, zmq.POLLIN]])

  loop
  {

    if (exit_flag == 1)
      break

    socks := poller.poll()

    if (socks[1]) {

      msg := socket.recv_string(zmq.DONTWAIT, "utf-8", false)
      if (msg is integer && msg < 0) {
        if (socket.errno() == zmq.EAGAIN)
          continue
        if (socket.errno() == zmq.EINTR)
          continue
      }

      thread_id := DllCall("kernel32.dll\GetCurrentThreadId")

      ToolTip, %thread_id%, 200, 200, 2

      SendMessage, WM_NULL, Object(msg), lp_process_ui_callback, , ahk_id %hwnd_main%

    }

  }

}

process_ui_callback(object_address)
{
  ;MsgBox, % arg1
  thread_id := DllCall("kernel32.dll\GetCurrentThreadId")

  ToolTip, %thread_id%, 500, 200, 3

  msg := Object(object_address)

  GuiControl, Text, TestText, % msg.1 " " msg.2 " " msg.3

  ObjRelease(object_address)

}

Gui, +AlwaysOnTop +hWndhwnd_main
Gui, Add, Text, w1200 vTestText, 等待回调UI
Gui, Add, Button, gStart, Start
Gui, Add, Button, gGoButton, Exit
Gui, Show, w1200 h100,接收zeromq消息

OnMessage(WM_NULL, "ON_WM_NULL")

return

ON_WM_NULL(wParam, lParam)
{
  if (lParam != 0 && wParam != 0) {

    DllCall(lParam, "Ptr", wParam)

  }

}

Start:
  if (!hThread)
    hThread := DllCall("CreateThread", Ptr, 0, Ptr, 0, Ptr, lp_recevie_message, Ptr, 0, UInt, 0, Ptr, 0)

return

GoButton:
  exit_flag := 1
  DllCall("WaitForSingleObject", "PTR", hThread, "UInt", 0xFFFFFFFF)
  DllCall("CloseHandle", "PTR", hThread)

  DllCall("DeleteCriticalSection","Ptr", RTL_CRITICAL_SECTION)

ExitApp
return

#Include %A_LineFile%\..\..\ZeroMQ\ZeroMQ.ahk




malcev
Posts: 1769
Joined: 12 Aug 2014, 12:37

Re: RegisterSyncCallback (for multi-threaded APIs)

20 Dec 2022, 23:46

@lexikos, can You please explain and may be give some faq how do You get this code?

Code: Select all

        /*
        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)
        */
lexikos
Posts: 9553
Joined: 30 Sep 2013, 04:07
Contact:

Re: RegisterSyncCallback (for multi-threaded APIs)

21 Dec 2022, 00:37

The steps to "getting" that code were:
  1. Understand the calling convention.
  2. Understand the CPU instructions needed to take the parameters and pass them via SendMessage.
  3. Assemble or compile some code, then disassemble it to get the machine code.
  4. Take the machine code and translate it to NumPut calls (see the x64 code), or put it together one instruction at a time with NumPut (see the x86 code).

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: No registered users and 147 guests