golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.97k stars 17.66k forks source link

runtime: Windows callbacks are not working #59650

Closed vault-thirteen closed 1 year ago

vault-thirteen commented 1 year ago

What version of Go are you using (go version)?

1.20.3.

Does this issue reproduce with the latest release?

Yes.

What operating system and processor architecture are you using (go env)?

Windows 10, Intel x86-64.

What did you do?

I create a callback and try to get it working. I create a hook method to listen to system's events from a keyboard. I send some keyboard events, but the callback is not started. When the callback should be "fired", it does not "fire".

Code example is following.

package main

import "C"
import (
    "fmt"
    "log"
    "time"

    "golang.org/x/sys/windows"
)

var (
    kernel32DLL = windows.NewLazySystemDLL("kernel32.dll")
    user32DLL   = windows.NewLazySystemDLL("user32.dll")

    procGetCurrentThreadId = kernel32DLL.NewProc("GetCurrentThreadId")
    procSetWindowsHookExW  = user32DLL.NewProc("SetWindowsHookExW")
    procCallNextHookEx     = user32DLL.NewProc("CallNextHookEx")
)

var hook uintptr

const (
    // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowshookexw
    WH_KEYBOARD    = 2
    WH_KEYBOARD_LL = 13
)

func mustBeNoError(err error) {
    if err != nil {
        panic(err)
    }
}

func main() {
    var err = kernel32DLL.Load()
    mustBeNoError(err)
    defer func() {
        derr := windows.FreeLibrary(windows.Handle(kernel32DLL.Handle()))
        if derr != nil {
            log.Println(derr)
        }
    }()

    err = user32DLL.Load()
    mustBeNoError(err)
    defer func() {
        derr := windows.FreeLibrary(windows.Handle(user32DLL.Handle()))
        if derr != nil {
            log.Println(derr)
        }
    }()

    threadId, _, _ := procGetCurrentThreadId.Call()
    fmt.Println("ThreadId:", threadId)

    //callback := syscall.NewCallback(callbackFunc)
    callback := windows.NewCallback(callbackFunc)
    defer func() {
        // Callback release is not available in Golang.
        // What should I do when the limit is reached ? ...
    }()

    hook, _, _ = procSetWindowsHookExW.Call(WH_KEYBOARD, callback, 0, threadId)
    fmt.Println("hook:", hook)

    time.Sleep(time.Second * 10)
}

func callbackFunc(code, wParam, lParam uintptr) uintptr {
    ret, _, _ := procCallNextHookEx.Call(hook, code, wParam, lParam)
    fmt.Println("inside callbackFunc")
    return ret
}

I tried:

but the result stays the same. It does not work.

What did you expect to see?

I expect to see working callbacks on Windows O.S. in Golang.

What did you see instead?

I see broken callbacks on Windows O.S. in Golang.

Program's output was:

ThreadId: 12984
hook: 30737909
ianlancetaylor commented 1 year ago

CC @golang/windows

qmuntal commented 1 year ago

I don't thing there is a problem with Windows callbacks, but with your code @vault-thirteen. It could be that you are missing the message loop that processes incoming messages at the end.

Here is a code example that exercises the callback when the mouse is moves:

package main

import "C"
import (
    "syscall"
    "unsafe"
)

var (
    moduser32 = syscall.NewLazyDLL("user32.dll")

    procSetWindowsHookEx = moduser32.NewProc("SetWindowsHookExW")
    procGetMessage       = moduser32.NewProc("GetMessageW")
    procTranslateMessage = moduser32.NewProc("TranslateMessage")
    procDispatchMessage  = moduser32.NewProc("DispatchMessageW")
)

const WH_MOUSE_LL = 14

type MSG struct {
    Hwnd    uint32
    Message uint32
    WParam  uintptr
    LParam  uintptr
    Time    uint32
    Pt      [2]int32
}

func main() {
    hook, _, _ := procSetWindowsHookEx.Call(WH_MOUSE_LL, uintptr(syscall.NewCallback(callbackFunc)), 0, 0)
    println(hook)
    var msg MSG
    for {
        procGetMessage.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0)
        procTranslateMessage.Call(uintptr(unsafe.Pointer(&msg)))
        procDispatchMessage.Call(uintptr(unsafe.Pointer(&msg)))
    }
}

func callbackFunc(code int, wParam uintptr, lParam uintptr) uintptr {
    println("inside callbackFunc")
    return 0
}
vault-thirteen commented 1 year ago

@qmuntal , why are you using 0 as a ThreadId ?

qmuntal commented 1 year ago

@qmuntal , why are you using 0 as a ThreadId ?

WH_MOUSE_LL is a global hook, so you must pass 0 as threadid. See SetWindowsHookEx remarks.

vault-thirteen commented 1 year ago

I am testing it with WH_KEYBOARD and provide a ThreadId. The code is blocked waiting for a message, i.e. the GetMessage function waits forever and does not return.

Program's output is following.

Callback_Test.exe
ThreadId: 2604
hook: 64422195

The code is following.

package main

import "C"
import (
    "fmt"
    "log"
    "time"
    "unsafe"

    "golang.org/x/sys/windows"
)

var (
    kernel32DLL = windows.NewLazySystemDLL("kernel32.dll")
    user32DLL   = windows.NewLazySystemDLL("user32.dll")

    procGetCurrentThreadId  = kernel32DLL.NewProc("GetCurrentThreadId")
    procSetWindowsHookExW   = user32DLL.NewProc("SetWindowsHookExW")
    procUnhookWindowsHookEx = user32DLL.NewProc("UnhookWindowsHookEx")
    procCallNextHookEx      = user32DLL.NewProc("CallNextHookEx")
    procGetMessageW         = user32DLL.NewProc("GetMessageW")
    procTranslateMessage    = user32DLL.NewProc("TranslateMessage")
    procDispatchMessageW    = user32DLL.NewProc("DispatchMessageW")
)

var hook uintptr

const (
    // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowshookexw
    WH_KEYBOARD    = 2  // Scope: Global or Thread.
    WH_MOUSE       = 7  // Scope: Global or Thread.
    WH_KEYBOARD_LL = 13 // Scope: Global.
    WH_MOUSE_LL    = 14 // Scope: Global.
)

type TagMSG struct {
    hwnd    uint32  // HWND hwnd;
    message uint32  // UINT message;
    wParam  uintptr // WPARAM wParam;
    lParam  uintptr // LPARAM lParam;
    time    uint32  // DWORD time; typedef unsigned long DWORD;
    pt      Point   // POINT pt;
}

type Point struct {
    x int32 // LONG x; typedef long LONG;
    y int32 // LONG y; typedef long LONG;
}

// https://learn.microsoft.com/en-us/cpp/cpp/data-type-ranges?view=msvc-170

func mustBeNoError(err error) {
    if err != nil {
        panic(err)
    }
}

func main() {
    var err = kernel32DLL.Load()
    mustBeNoError(err)
    defer func() {
        derr := windows.FreeLibrary(windows.Handle(kernel32DLL.Handle()))
        if derr != nil {
            log.Println(derr)
        }
    }()

    err = user32DLL.Load()
    mustBeNoError(err)
    defer func() {
        derr := windows.FreeLibrary(windows.Handle(user32DLL.Handle()))
        if derr != nil {
            log.Println(derr)
        }
    }()

    threadId, _, _ := procGetCurrentThreadId.Call()
    fmt.Println("ThreadId:", threadId)

    //callback := syscall.NewCallback(callbackFunc)
    callback := windows.NewCallback(callbackFunc)
    defer func() {
        // Callback release is not available in Golang.
        // What should I do when the limit is reached ? ...
    }()

    hook, _, _ = procSetWindowsHookExW.Call(WH_KEYBOARD, callback, 0, threadId)
    fmt.Println("hook:", hook)
    defer func() {
        ret, _, _ := procUnhookWindowsHookEx.Call(hook)
        fmt.Println("UnhookWindowsHookEx:", int32(ret))
    }()

    var msg TagMSG
    for {
        if GetMessage(&msg) == 0 {
            fmt.Println("break")
            break
        }
        fmt.Println("msg:", msg)
        if TranslateMessage(&msg) == 0 {
            fmt.Println("TranslateMessage error")
            break
        }
        lResult := DispatchMessage(&msg)
        fmt.Println("lResult:", lResult)
    }

    time.Sleep(time.Second * 5)
}

func callbackFunc(code, wParam, lParam uintptr) uintptr {
    fmt.Println("inside callbackFunc")
    ret, _, _ := procCallNextHookEx.Call(hook, code, wParam, lParam)
    return ret
}

func GetMessage(msg *TagMSG) int32 { // BOOL; typedef int BOOL;
    ret, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0)
    return int32(ret)
}

func TranslateMessage(msg *TagMSG) int32 { // BOOL; typedef int BOOL;
    ret, _, _ := procTranslateMessage.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0)
    return int32(ret)
}

func DispatchMessage(msg *TagMSG) (lResult uintptr) { // LRESULT; typedef LONG_PTR LRESULT;
    lResult, _, _ = procDispatchMessageW.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0)
    return lResult
}
vault-thirteen commented 1 year ago

@qmuntal , please, use a WH_KEYBOARD and provide a ThreadId as in my previous post. Will it work on your machine ?

qmuntal commented 1 year ago

@qmuntal , please, use a WH_KEYBOARD and provide a ThreadId as in my previous post. Will it work on your machine ?

It doesn't work with WH_KEYBOARD on my machine, but I still think it's not due to a bug in the windows callback, but because the SetWindowsHookExW is used in the wrong way (that API is awfully complicated...). Could you try creating a windows (i.e. using CreateWindowExW) and passing its handle as the hmod parameter? It can be that SetWindowsHookExW can't intercept messages created from terminal applications.

vault-thirteen commented 1 year ago

@qmuntal , thanks for the test. I will make this experiment. I need some time to read the Microsoft's API and I will check it.

vault-thirteen commented 1 year ago

It looks like something is going wrong. I get a NULL pointer after the creation of a Window. @qmuntal

ThreadId: 10816
sizeOfX: 80
RegisterClass: 49828
hWnd: 0
SetActiveWindow: 0
ShowWindow: 0  
UpdateWindow: 0
hook: 256050767

The code is following.

package main

import (
    "fmt"
    "log"
    "time"
    "unsafe"

    "golang.org/x/sys/windows"
)

var (
    kernel32DLL = windows.NewLazySystemDLL("kernel32.dll")
    user32DLL   = windows.NewLazySystemDLL("user32.dll")

    procGetCurrentThreadId  = kernel32DLL.NewProc("GetCurrentThreadId")
    procSetWindowsHookExW   = user32DLL.NewProc("SetWindowsHookExW")
    procUnhookWindowsHookEx = user32DLL.NewProc("UnhookWindowsHookEx")
    procCallNextHookEx      = user32DLL.NewProc("CallNextHookEx")
    procGetMessageW         = user32DLL.NewProc("GetMessageW")
    procTranslateMessage    = user32DLL.NewProc("TranslateMessage")
    procDispatchMessageW    = user32DLL.NewProc("DispatchMessageW")
    procRegisterClassExW    = user32DLL.NewProc("RegisterClassExW")
    procCreateWindowExW     = user32DLL.NewProc("CreateWindowExW")
    procDestroyWindow       = user32DLL.NewProc("DestroyWindow")
    procSetActiveWindow     = user32DLL.NewProc("SetActiveWindow")
    procShowWindow          = user32DLL.NewProc("ShowWindow")
    procUpdateWindow        = user32DLL.NewProc("UpdateWindow")
)

var hook uintptr

const (
    // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowshookexw
    WH_KEYBOARD    = 2  // Scope: Global or Thread.
    WH_MOUSE       = 7  // Scope: Global or Thread.
    WH_KEYBOARD_LL = 13 // Scope: Global.
    WH_MOUSE_LL    = 14 // Scope: Global.
)

const (
    // https://learn.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles
    WS_EX_ACCEPTFILES uint32 = 0x00000010
    WS_EX_APPWINDOW   uint32 = 0x00040000

    // https://learn.microsoft.com/en-us/windows/win32/winmsg/window-styles
    WS_THICKFRAME uint32 = 0x00040000
    WS_SIZEBOX    uint32 = 0x00040000
    WS_CAPTION    uint32 = 0x00C00000
    WS_VISIBLE    uint32 = 0x10000000

    // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
    SW_SHOWNORMAL int32 = 1

    // https://learn.microsoft.com/ru-ru/windows/win32/winmsg/window-class-styles
    CS_VREDRAW uint16 = 0x0001
    CS_HREDRAW uint16 = 0x0002
    CS_CLASSDC uint16 = 0x0040
)

/*
typedef struct tagWNDCLASSEXW {
  UINT      cbSize;
  UINT      style;
  WNDPROC   lpfnWndProc;
  int       cbClsExtra;
  int       cbWndExtra;
  HINSTANCE hInstance;
  HICON     hIcon;
  HCURSOR   hCursor;
  HBRUSH    hbrBackground;
  LPCWSTR   lpszMenuName;
  LPCWSTR   lpszClassName;
  HICON     hIconSm;
} WNDCLASSEXW, *PWNDCLASSEXW, *NPWNDCLASSEXW, *LPWNDCLASSEXW;
*/
// typedef _Null_terminated_ CONST CHAR *LPCSTR, *PCSTR;
type WNDCLASSEXW struct {
    cbSize        uint32
    style         uint32
    lpfnWndProc   uintptr // WNDPROC
    cbClsExtra    int32
    cbWndExtra    int32
    hInstance     HINSTANCE
    hIcon         HICON
    hCursor       HCURSOR
    hbrBackground HBRUSH
    lpszMenuName  uintptr // LPCSTR
    lpszClassName uintptr // LPCSTR
    hIconSm       HICON
}

type HINSTANCE = HANDLE
type HICON = HANDLE
type HCURSOR = HICON
type HBRUSH = HANDLE
type HANDLE = windows.Handle

type TagMSG struct {
    hwnd    uint32  // HWND hwnd;
    message uint32  // UINT message;
    wParam  uintptr // WPARAM wParam;
    lParam  uintptr // LPARAM lParam;
    time    uint32  // DWORD time; typedef unsigned long DWORD;
    pt      Point   // POINT pt;
}

type Point struct {
    x int32 // LONG x; typedef long LONG;
    y int32 // LONG y; typedef long LONG;
}

// https://learn.microsoft.com/en-us/cpp/cpp/data-type-ranges?view=msvc-170

func mustBeNoError(err error) {
    if err != nil {
        panic(err)
    }
}

func main() {
    var err = kernel32DLL.Load()
    mustBeNoError(err)
    defer func() {
        derr := windows.FreeLibrary(windows.Handle(kernel32DLL.Handle()))
        if derr != nil {
            log.Println(derr)
        }
    }()

    err = user32DLL.Load()
    mustBeNoError(err)
    defer func() {
        derr := windows.FreeLibrary(windows.Handle(user32DLL.Handle()))
        if derr != nil {
            log.Println(derr)
        }
    }()

    threadId, _, _ := procGetCurrentThreadId.Call()
    fmt.Println("ThreadId:", threadId)

    windowClassName := "MainWClass"
    var x WNDCLASSEXW
    sizeOfX := unsafe.Sizeof(x)
    fmt.Println("sizeOfX:", sizeOfX)
    x = WNDCLASSEXW{
        cbSize:        uint32(sizeOfX), // The size, in bytes, of this structure. Set this member to sizeof(WNDCLASSEX).
        style:         uint32(CS_VREDRAW | CS_HREDRAW | CS_CLASSDC),
        lpfnWndProc:   0,
        cbClsExtra:    0, // The number of extra bytes to allocate following the window-class structure.
        cbWndExtra:    0, // The number of extra bytes to allocate following the window instance.
        hInstance:     0,
        hIcon:         0,
        hCursor:       0,
        hbrBackground: 0,
        lpszMenuName:  0,
        lpszClassName: uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(windowClassName))),
        hIconSm:       0,
    }
    fmt.Println("RegisterClass:", RegisterClass(&x))

    var hWnd = CreateWindow(
        WS_EX_ACCEPTFILES|WS_EX_APPWINDOW,
        uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(windowClassName))),
        uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("Test Window"))),
        WS_VISIBLE|WS_THICKFRAME|WS_CAPTION,
        0, 0, 640, 480, 0, 0, 0, 0,
    )
    fmt.Println("hWnd:", hWnd)
    defer func() {
        fmt.Println("DestroyWindow:", DestroyWindow(hWnd))
    }()
    fmt.Println("SetActiveWindow:", SetActiveWindow(hWnd))
    fmt.Println("ShowWindow:", ShowWindow(hWnd, SW_SHOWNORMAL))
    fmt.Println("UpdateWindow:", UpdateWindow(hWnd))

    //callback := syscall.NewCallback(callbackFunc)
    callback := windows.NewCallback(callbackFunc)
    defer func() {
        // Callback release is not available in Golang.
        // What should I do when the limit is reached ? ...
    }()

    hook, _, _ = procSetWindowsHookExW.Call(WH_KEYBOARD, callback, 0, threadId)
    fmt.Println("hook:", hook)
    defer func() {
        ret, _, _ := procUnhookWindowsHookEx.Call(hook)
        fmt.Println("UnhookWindowsHookEx:", int32(ret))
    }()

    var msg TagMSG
    for {
        if GetMessage(&msg) == 0 {
            fmt.Println("break")
            break
        }
        fmt.Println("msg:", msg)
        if TranslateMessage(&msg) == 0 {
            fmt.Println("TranslateMessage error")
            break
        }
        lResult := DispatchMessage(&msg)
        fmt.Println("lResult:", lResult)
    }

    time.Sleep(time.Second * 5)
}

func callbackFunc(code, wParam, lParam uintptr) uintptr {
    fmt.Println("inside callbackFunc")
    ret, _, _ := procCallNextHookEx.Call(hook, code, wParam, lParam)
    return ret
}

func GetMessage(msg *TagMSG) int32 { // BOOL; typedef int BOOL;
    ret, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0)
    return int32(ret)
}

func TranslateMessage(msg *TagMSG) int32 { // BOOL; typedef int BOOL;
    ret, _, _ := procTranslateMessage.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0)
    return int32(ret)
}

func DispatchMessage(msg *TagMSG) (lResult uintptr) { // LRESULT; typedef LONG_PTR LRESULT;
    lResult, _, _ = procDispatchMessageW.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0)
    return lResult
}

// ATOM RegisterClassExW([in] const WNDCLASSEXW *unnamedParam1);
func RegisterClass(unnamedParam1 *WNDCLASSEXW) uintptr {
    atom, _, _ := procRegisterClassExW.Call(uintptr(unsafe.Pointer(unnamedParam1)))
    return atom
}

// HWND CreateWindowExW([in] DWORD dwExStyle, [in, optional] LPCWSTR lpClassName,[in, optional] LPCWSTR lpWindowName, [in] DWORD dwStyle, [in] int X, [in] int Y, [in] int nWidth, [in] int nHeight, [in, optional] HWND hWndParent, [in, optional] HMENU hMenu, [in, optional] HINSTANCE hInstance, [in, optional] LPVOID lpParam);
// typedef unsigned long DWORD; typedef _Null_terminated_ CONST WCHAR *LPCWSTR, *PCWSTR;
func CreateWindow(dwExStyle uint32, lpClassName uintptr, lpWindowName uintptr, dwStyle uint32, x int16, y int16, nWidth int16, nHeight int16, hWndParent uintptr, hMenu uintptr, hInstance uintptr, lpParam uintptr) uintptr {
    hWnd, _, _ := procCreateWindowExW.Call()
    return hWnd
}

// BOOL DestroyWindow([in] HWND hWnd); typedef int BOOL;
func DestroyWindow(hWnd uintptr) int32 {
    ret, _, _ := procDestroyWindow.Call(hWnd)
    return int32(ret)
}

// HWND SetActiveWindow([in] HWND hWnd);
func SetActiveWindow(hWnd uintptr) (topLevelWindow uintptr) {
    topLevelWindow, _, _ = procSetActiveWindow.Call(hWnd)
    return topLevelWindow
}

// BOOL ShowWindow([in] HWND hWnd,[in] int nCmdShow); typedef int BOOL;
func ShowWindow(hWnd uintptr, nCmdShow int32) int32 {
    ret, _, _ := procShowWindow.Call(hWnd, uintptr(nCmdShow))
    return int32(ret)
}

// BOOL UpdateWindow([in] HWND hWnd); typedef int BOOL;
func UpdateWindow(hWnd uintptr) int32 {
    ret, _, _ := procUpdateWindow.Call(hWnd)
    return int32(ret)
}
qmuntal commented 1 year ago

@vault-thirteen the CreateWindow function is not setting the procCreateWindowExW parameters and the the WNDCLASSEXW is missing the mandatory lpfnWndProc parameter. Something like this should be enough:

precDefWindowProc       = user32DLL.NewProc("DefWindowProcW")
...
x = WNDCLASSEXW{
    cbSize:        uint32(sizeOfX), // The size, in bytes, of this structure. Set this member to sizeof(WNDCLASSEX).
    lpszClassName: uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(windowClassName))),
    lpfnWndProc: syscall.NewCallback(func(window uintptr, msg uintptr, w, l uintptr) uintptr {
        ret, _, _ := precDefWindowProc.Call(window, msg, w, l)
        return ret
    }),
}
vault-thirteen commented 1 year ago

@qmuntal , thank you very much for help. It looks like, callbacks are working !

Callback_Test.exe ThreadId: 9340 sizeOfX: 80 RegisterClass: 49790 hWnd: 264238 SetActiveWindow: 264238 ShowWindow: 24 UpdateWindow: 1 hook: 329777 inside callbackFunc wParam: 13, lParam:3223060481 TranslateMessage: success inside callbackFunc wParam: 72, lParam:2293761 TranslateMessage: success inside callbackFunc wParam: 72, lParam:3223519233 TranslateMessage: success

I have one question left. I need to unhook the callback. For some reason I am not receiving the WM_QUIT (zero result of GetMessageW function) when I close the application.

package main

import (
    "fmt"
    "log"
    "unsafe"

    "golang.org/x/sys/windows"
)

var (
    kernel32DLL = windows.NewLazySystemDLL("kernel32.dll")
    user32DLL   = windows.NewLazySystemDLL("user32.dll")

    procGetCurrentThreadId = kernel32DLL.NewProc("GetCurrentThreadId")

    procCallNextHookEx      = user32DLL.NewProc("CallNextHookEx")
    procCreateWindowExW     = user32DLL.NewProc("CreateWindowExW")
    procDefWindowProcW      = user32DLL.NewProc("DefWindowProcW")
    procDestroyWindow       = user32DLL.NewProc("DestroyWindow")
    procDispatchMessageW    = user32DLL.NewProc("DispatchMessageW")
    procGetMessageW         = user32DLL.NewProc("GetMessageW")
    procRegisterClassExW    = user32DLL.NewProc("RegisterClassExW")
    procSetActiveWindow     = user32DLL.NewProc("SetActiveWindow")
    procSetWindowsHookExW   = user32DLL.NewProc("SetWindowsHookExW")
    procShowWindow          = user32DLL.NewProc("ShowWindow")
    procTranslateMessage    = user32DLL.NewProc("TranslateMessage")
    procUnhookWindowsHookEx = user32DLL.NewProc("UnhookWindowsHookEx")
    procUpdateWindow        = user32DLL.NewProc("UpdateWindow")
)

var hook uintptr

const (
    // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowshookexw
    WH_KEYBOARD    = 2  // Scope: Global or Thread.
    WH_MOUSE       = 7  // Scope: Global or Thread.
    WH_KEYBOARD_LL = 13 // Scope: Global.
    WH_MOUSE_LL    = 14 // Scope: Global.
)

const (
    // https://learn.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles
    WS_EX_ACCEPTFILES uint32 = 0x00000010
    WS_EX_APPWINDOW   uint32 = 0x00040000

    // https://learn.microsoft.com/en-us/windows/win32/winmsg/window-styles
    WS_THICKFRAME uint32 = 0x00040000
    WS_SIZEBOX    uint32 = 0x00040000
    WS_CAPTION    uint32 = 0x00C00000
    WS_VISIBLE    uint32 = 0x10000000

    // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
    SW_SHOWNORMAL int32 = 1

    // https://learn.microsoft.com/ru-ru/windows/win32/winmsg/window-class-styles
    CS_VREDRAW uint16 = 0x0001
    CS_HREDRAW uint16 = 0x0002
    CS_CLASSDC uint16 = 0x0040

    // https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms644975(v=vs.85)
    HC_ACTION = 0
)

/*
typedef struct tagWNDCLASSEXW {
  UINT      cbSize;
  UINT      style;
  WNDPROC   lpfnWndProc;
  int       cbClsExtra;
  int       cbWndExtra;
  HINSTANCE hInstance;
  HICON     hIcon;
  HCURSOR   hCursor;
  HBRUSH    hbrBackground;
  LPCWSTR   lpszMenuName;
  LPCWSTR   lpszClassName;
  HICON     hIconSm;
} WNDCLASSEXW, *PWNDCLASSEXW, *NPWNDCLASSEXW, *LPWNDCLASSEXW;
*/
// typedef _Null_terminated_ CONST CHAR *LPCSTR, *PCSTR;
type WNDCLASSEXW struct {
    cbSize        uint32
    style         uint32
    lpfnWndProc   uintptr // WNDPROC
    cbClsExtra    int32
    cbWndExtra    int32
    hInstance     HINSTANCE
    hIcon         HICON
    hCursor       HCURSOR
    hbrBackground HBRUSH
    lpszMenuName  uintptr // LPCSTR
    lpszClassName uintptr // LPCSTR
    hIconSm       HICON
}

type HINSTANCE = HANDLE
type HICON = HANDLE
type HCURSOR = HICON
type HBRUSH = HANDLE
type HANDLE = windows.Handle
type LPARAM = uintptr
type WPARAM = uintptr
type LRESULT = uintptr
type HWND = uint32

type TagMSG struct {
    hwnd    HWND   // HWND hwnd;
    message uint32 // UINT message;
    wParam  WPARAM // WPARAM wParam;
    lParam  LPARAM // LPARAM lParam;
    time    uint32 // DWORD time; typedef unsigned long DWORD;
    pt      Point  // POINT pt;
}

type Point struct {
    x int32 // LONG x; typedef long LONG;
    y int32 // LONG y; typedef long LONG;
}

// https://learn.microsoft.com/en-us/cpp/cpp/data-type-ranges?view=msvc-170

func mustBeNoError(err error) {
    if err != nil {
        panic(err)
    }
}

func main() {
    var err = kernel32DLL.Load()
    mustBeNoError(err)
    defer func() {
        derr := windows.FreeLibrary(windows.Handle(kernel32DLL.Handle()))
        if derr != nil {
            log.Println(derr)
        }
    }()

    err = user32DLL.Load()
    mustBeNoError(err)
    defer func() {
        derr := windows.FreeLibrary(windows.Handle(user32DLL.Handle()))
        if derr != nil {
            log.Println(derr)
        }
    }()

    threadId, _, _ := procGetCurrentThreadId.Call()
    fmt.Println("ThreadId:", threadId)

    windowClassName := "MainWClass"
    var x WNDCLASSEXW
    sizeOfX := unsafe.Sizeof(x)
    fmt.Println("sizeOfX:", sizeOfX)
    x = WNDCLASSEXW{
        cbSize:        uint32(sizeOfX), // The size, in bytes, of this structure. Set this member to sizeof(WNDCLASSEX).
        style:         uint32(CS_VREDRAW | CS_HREDRAW | CS_CLASSDC),
        lpfnWndProc:   windows.NewCallback(DefWindowProcW),
        cbClsExtra:    0, // The number of extra bytes to allocate following the window-class structure.
        cbWndExtra:    0, // The number of extra bytes to allocate following the window instance.
        hInstance:     0,
        hIcon:         0,
        hCursor:       0,
        hbrBackground: 0,
        lpszMenuName:  0,
        lpszClassName: uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(windowClassName))),
        hIconSm:       0,
    }
    fmt.Println("RegisterClass:", RegisterClass(&x))

    var hWnd = CreateWindow(
        WS_EX_ACCEPTFILES|WS_EX_APPWINDOW,
        uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(windowClassName))),
        uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("Test Window"))),
        WS_VISIBLE|WS_THICKFRAME|WS_CAPTION,
        0, 0, 640, 480, 0, 0, 0, 0,
    )
    fmt.Println("hWnd:", hWnd)
    defer func() {
        fmt.Println("DestroyWindow:", DestroyWindow(hWnd))
    }()
    fmt.Println("SetActiveWindow:", SetActiveWindow(hWnd))
    fmt.Println("ShowWindow:", ShowWindow(hWnd, SW_SHOWNORMAL))
    fmt.Println("UpdateWindow:", UpdateWindow(hWnd))

    //callback := syscall.NewCallback(callbackFunc)
    callback := windows.NewCallback(callbackFunc)
    defer func() {
        // Callback release is not available in Golang.
        // What should I do when the limit is reached ? ...
    }()

    hook, _, _ = procSetWindowsHookExW.Call(WH_KEYBOARD, callback, 0, threadId)
    fmt.Println("hook:", hook)
    defer func() {
        ret, _, _ := procUnhookWindowsHookEx.Call(hook)
        fmt.Println("UnhookWindowsHookEx:", int32(ret))
    }()

    var msg TagMSG
    for {
        // If the function retrieves a message other than WM_QUIT, the return value is nonzero.
        // If the function retrieves the WM_QUIT message, the return value is zero.
        if GetMessage(&msg) == 0 {
            fmt.Println("break")
            break
        }
        if TranslateMessage(&msg) != 0 {
            fmt.Println("TranslateMessage: success")
        }
        // Although its meaning depends on the message being dispatched, the return value generally is ignored.
        _ = DispatchMessage(&msg)
    }

    fmt.Println("Fin")
}

// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms644975(v=vs.85)
func callbackFunc(nCode int32, wParam WPARAM, lParam LPARAM) (ret uintptr) {
    fmt.Println("inside callbackFunc")

    if nCode == HC_ACTION {
        fmt.Println(fmt.Sprintf("wParam: %v, lParam:%v", wParam, lParam))
    }

    ret, _, _ = procCallNextHookEx.Call(hook, uintptr(nCode), wParam, lParam)
    return ret
}

func GetMessage(msg *TagMSG) int32 { // BOOL; typedef int BOOL;
    ret, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0)
    return int32(ret)
}

func TranslateMessage(msg *TagMSG) int32 { // BOOL; typedef int BOOL;
    ret, _, _ := procTranslateMessage.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0)
    return int32(ret)
}

// LRESULT DispatchMessageW([in] const MSG *lpMsg);
func DispatchMessage(msg *TagMSG) (lResult LRESULT) { // LRESULT; typedef LONG_PTR LRESULT;
    lResult, _, _ = procDispatchMessageW.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0)
    return lResult
}

// ATOM RegisterClassExW([in] const WNDCLASSEXW *unnamedParam1);
func RegisterClass(unnamedParam1 *WNDCLASSEXW) uintptr {
    atom, _, _ := procRegisterClassExW.Call(uintptr(unsafe.Pointer(unnamedParam1)))
    return atom
}

// HWND CreateWindowExW([in] DWORD dwExStyle, [in, optional] LPCWSTR lpClassName,[in, optional] LPCWSTR lpWindowName, [in] DWORD dwStyle, [in] int X, [in] int Y, [in] int nWidth, [in] int nHeight, [in, optional] HWND hWndParent, [in, optional] HMENU hMenu, [in, optional] HINSTANCE hInstance, [in, optional] LPVOID lpParam);
// typedef unsigned long DWORD; typedef _Null_terminated_ CONST WCHAR *LPCWSTR, *PCWSTR;
func CreateWindow(dwExStyle uint32, lpClassName uintptr, lpWindowName uintptr, dwStyle uint32, x int16, y int16, nWidth int16, nHeight int16, hWndParent uintptr, hMenu uintptr, hInstance uintptr, lpParam uintptr) uintptr {
    hWnd, _, _ := procCreateWindowExW.Call(uintptr(dwExStyle), lpClassName, lpWindowName, uintptr(dwStyle), uintptr(x), uintptr(y), uintptr(nWidth), uintptr(nHeight), hWndParent, hMenu, hInstance, lpParam)
    return hWnd
}

// BOOL DestroyWindow([in] HWND hWnd); typedef int BOOL;
func DestroyWindow(hWnd uintptr) int32 {
    ret, _, _ := procDestroyWindow.Call(hWnd)
    return int32(ret)
}

// HWND SetActiveWindow([in] HWND hWnd);
func SetActiveWindow(hWnd uintptr) (topLevelWindow uintptr) {
    topLevelWindow, _, _ = procSetActiveWindow.Call(hWnd)
    return topLevelWindow
}

// BOOL ShowWindow([in] HWND hWnd,[in] int nCmdShow); typedef int BOOL;
func ShowWindow(hWnd uintptr, nCmdShow int32) int32 {
    ret, _, _ := procShowWindow.Call(hWnd, uintptr(nCmdShow))
    return int32(ret)
}

// BOOL UpdateWindow([in] HWND hWnd); typedef int BOOL;
func UpdateWindow(hWnd uintptr) int32 {
    ret, _, _ := procUpdateWindow.Call(hWnd)
    return int32(ret)
}

// LRESULT DefWindowProcW([in] HWND hWnd, [in] UINT Msg, [in] WPARAM wParam, [in] LPARAM lParam);
func DefWindowProcW(hWnd uint32, Msg uint32, wParam WPARAM, lParam LPARAM) LRESULT {
    lResult, _, _ := procDefWindowProcW.Call(uintptr(hWnd), uintptr(Msg), wParam, lParam)
    return lResult
}
qmuntal commented 1 year ago

I have one question left. I need to unhook the callback. For some reason I am not receiving the WM_QUIT (zero result of GetMessageW function) when I close the application.

I don't have an answer off the top of my head, but probably Stack Overflow will know 😸.

Closing as no issue, thanks for reporting it anyway @vault-thirteen.