Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

runtime: Windows callbacks are not working #59650

Closed
vault-thirteen opened this issue Apr 15, 2023 · 12 comments
Closed

runtime: Windows callbacks are not working #59650

vault-thirteen opened this issue Apr 15, 2023 · 12 comments
Assignees
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. FrozenDueToAge OS-Windows WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided.
Milestone

Comments

@vault-thirteen
Copy link

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:

  • callback := syscall.NewCallback(callbackFunc)
  • callback := windows.NewCallback(callbackFunc)
  • with import "C"
  • without import "C"
  • WH_KEYBOARD
  • WH_KEYBOARD_LL

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
@gopherbot gopherbot added the compiler/runtime Issues related to the Go compiler and/or runtime. label Apr 15, 2023
@gopherbot gopherbot added this to the Unreleased milestone Apr 15, 2023
@ianlancetaylor
Copy link
Contributor

CC @golang/windows

@qmuntal qmuntal added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Apr 18, 2023
@mknyszek mknyszek changed the title x/sys/windows: Callbacks are not working. x/sys/windows: callbacks are not working Apr 19, 2023
@mknyszek mknyszek self-assigned this Apr 19, 2023
@mknyszek mknyszek changed the title x/sys/windows: callbacks are not working runtime: callbacks are not working Apr 19, 2023
@mknyszek mknyszek changed the title runtime: callbacks are not working runtime: Windows callbacks are not working Apr 19, 2023
@qmuntal
Copy link
Contributor

qmuntal commented Apr 21, 2023

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
}

@qmuntal qmuntal assigned qmuntal and unassigned mknyszek Apr 21, 2023
@qmuntal qmuntal added WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. and removed NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. labels Apr 21, 2023
@vault-thirteen
Copy link
Author

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

@qmuntal
Copy link
Contributor

qmuntal commented Apr 22, 2023

@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
Copy link
Author

vault-thirteen commented Apr 22, 2023

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
Copy link
Author

vault-thirteen commented Apr 22, 2023

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

@qmuntal
Copy link
Contributor

qmuntal commented Apr 25, 2023

@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
Copy link
Author

@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
Copy link
Author

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
Copy link
Contributor

qmuntal commented May 3, 2023

@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
Copy link
Author

@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
Copy link
Contributor

qmuntal commented May 4, 2023

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.

@qmuntal qmuntal closed this as completed May 4, 2023
@golang golang locked and limited conversation to collaborators May 3, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. FrozenDueToAge OS-Windows WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided.
Projects
None yet
Development

No branches or pull requests

5 participants