Skip to content

syscall: unexplained crashes when using win32api (PostMessage + Window functions) #50872

Closed
@SeanTolstoyevski

Description

@SeanTolstoyevski

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

go version go1.17.3 windows/amd64

windows 10 64bit

Does this issue reproduce with the latest release?

I don't know, I haven't tried it with beta versions or older versions.

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

go env Output

set GO111MODULE=on
set GOARCH=amd64
set GOBIN=
set GOCACHE=C:\Users\dl\AppData\Local\go-build
set GOENV=C:\Users\dl\AppData\Roaming\go\env
set GOEXE=.exe
set GOEXPERIMENT=
set GOFLAGS=
set GOHOSTARCH=amd64
set GOHOSTOS=windows
set GOINSECURE=
set GOMODCACHE=C:\Users\dl\go\pkg\mod
set GONOPROXY=
set GONOSUMDB=
set GOOS=windows
set GOPATH=C:\Users\dl\go
set GOPRIVATE=
set GOPROXY=https://proxy.golang.org,direct
set GOROOT=c:\go
set GOSUMDB=sum.golang.org
set GOTMPDIR=
set GOTOOLDIR=c:\go\pkg\tool\windows_amd64
set GOVCS=
set GOVERSION=go1.17.3
set GCCGO=gccgo
set AR=ar
set CC=gcc
set CXX=g++
set CGO_ENABLED=1
set GOMOD=NUL
set CGO_CFLAGS=-g -O2
set CGO_CPPFLAGS=
set CGO_CXXFLAGS=-g -O2
set CGO_FFLAGS=-g -O2
set CGO_LDFLAGS=-g -O2
set PKG_CONFIG=pkg-config
set GOGCCFLAGS=-m64 -mthreads -fmessage-length=0 -fdebug-prefix-map=C:\Users\dl\AppData\Local\Temp\go-build2985149018=/tmp/go-build -gno-record-gcc-switches

What did you do?

After long test I decided to share this. In this way, maybe a solution can be found, or this is the cause of an error in Golang's runtime.

Here I am summarizing the issue so that everyone can understand what I want to do:
Let's create a simple Win32api application. I will share an example program below.
We don't use cgo. We will use syscall.

The Win32api function GetMessage blocks the program if there are no messages in the event queue. So if no event is created, the program waits forever for an event to occur.
So I'm sending messages to the queue with PostMessage. I do this with time.Ticker.
Now we have prevented the program from being blocked forever. We can receive the events we send with PostMessage from the queue.
As a note, PostMessage runs another goroutine. Using runtime.LockOSThread does not prevent the program from crashing.

The program can run without anything for a long time. Sometimes it crashes with no errors. Sometimes it works normally for a very long time, sometimes it crashes immediately. Finally, after a certain time, the program stops responding.
This made me think about the possibility of race condition. But there is no such thing.
I wrote a duplicate of the same code in C++. Win32api shows no problems. The program never crashes. I know that C++ and Golang are not the same thing. I just wanted to understand where the problem is.
So the program not crashing in C++ makes me think that Golang is doing something wrong for win32api in runtime.
My guess is that he is moving the runtime goroutines to different threads or dealing with a similar golang thing. Some win32api functions are sensitive to the called thread. In other words, the thread that the goroutine is connected to should not change. Maybe GC is deleting something important for win32api. I really don't know what's going on here.

I tried to keep this issue simple so anyone can generate it themselves but since win32api is a low level api it takes a long time to create something.

Those who want to confirm this example can run the program and wait. You can leave the program's window and switch to something else. Because sometimes it works right for too long for incomprehensible reasons.

I don't know how to test this in a simpler way.

Go code (go build app.go):

package main

import (
	"errors"
	"fmt"
	"syscall"
	"unsafe"

	"time"
	"runtime"
)

var (
	k32 = syscall.NewLazyDLL("kernel32.dll")

	procGetModuleHandle         = k32.NewProc("GetModuleHandleW")
)

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

	procRegisterClassEx            = u32.NewProc("RegisterClassExW")
	procUpdateWindow               = u32.NewProc("UpdateWindow")
	procCreateWindowEx             = u32.NewProc("CreateWindowExW")
	procDestroyWindow              = u32.NewProc("DestroyWindow")
	procDefWindowProc              = u32.NewProc("DefWindowProcW")
	procPostQuitMessage            = u32.NewProc("PostQuitMessage")
	procGetMessage                 = u32.NewProc("GetMessageW")
	procTranslateMessage           = u32.NewProc("TranslateMessage")
	procDispatchMessage            = u32.NewProc("DispatchMessageW")
	procPostMessage                = u32.NewProc("PostMessageW")
)


func RegisterClassEx(wndClassEx *WNDCLASSEX) ATOM {
	ret, _, _ := procRegisterClassEx.Call(uintptr(unsafe.Pointer(wndClassEx)))
	return ATOM(ret)
}


func UpdateWindow(hwnd HWND) bool {
	ret, _, _ := procUpdateWindow.Call(
		uintptr(hwnd))
	return ret != 0
}

func CreateWindowEx(exStyle uint, className, windowName *uint16,
	style uint, x, y, width, height int, parent HWND, menu HMENU,
	instance HINSTANCE, param unsafe.Pointer) HWND {
	ret, _, _ := procCreateWindowEx.Call(uintptr(exStyle), uintptr(unsafe.Pointer(className)),
		uintptr(unsafe.Pointer(windowName)),
		uintptr(style),
		uintptr(x),
		uintptr(y),
		uintptr(width),
		uintptr(height),
		uintptr(parent),
		uintptr(menu),
		uintptr(instance),
		uintptr(param))
	return HWND(ret)
}

func DestroyWindow(hwnd HWND) bool {
	ret, _, _ := procDestroyWindow.Call(
		uintptr(hwnd))
	return ret != 0
}

func DefWindowProc(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr {
	ret, _, _ := procDefWindowProc.Call(uintptr(hwnd), uintptr(msg), wParam, lParam)
	return ret
}

func PostMessage(hwnd HWND, msg uint32, wParam, lParam uintptr) bool {
	ret, _, _ := procPostMessage.Call(uintptr(hwnd), uintptr(msg), wParam, lParam)
	return ret != 0
}

func TranslateMessage(msg *MSG) bool {
	ret, _, _ := procTranslateMessage.Call(
		uintptr(unsafe.Pointer(msg)))
	return ret != 0
}

func DispatchMessage(msg *MSG) uintptr {
	ret, _, _ := procDispatchMessage.Call(
		uintptr(unsafe.Pointer(msg)))
	return ret
}

func GetMessage(msg *MSG, hwnd HWND, msgFilterMin, msgFilterMax uint32) int {
	ret, _, _ := procGetMessage.Call( uintptr(unsafe.Pointer(msg)), uintptr(hwnd), uintptr(msgFilterMin), uintptr(msgFilterMax))
	return int(ret)
}


func PostQuitMessage(exitCode int) {
	procPostQuitMessage.Call(
		uintptr(exitCode))
}

func GetModuleHandle(lpModuleName *uint16) HINSTANCE {
	ret, _, _ := procGetModuleHandle.Call(uintptr(unsafe.Pointer(lpModuleName)))
	return HINSTANCE(ret)
}

type (
	ATOM uint16

	DWORD uint32

	HANDLE uintptr

	HINSTANCE HANDLE

	HMODULE HANDLE

	HRESULT int32

	HWND HANDLE

	LPARAM uintptr

	HKL     HANDLE

	LPCVOID unsafe.Pointer

	LRESULT uintptr

	PVOID unsafe.Pointer

	QPC_TIME uint64

	ULONG_PTR uintptr

	WPARAM uintptr

	HCURSOR HANDLE

	HICON HANDLE

	HMENU HANDLE


	HHOOK HANDLE

	HRAWINPUT HANDLE

	COLORREF uint32

	HBITMAP HGDIOBJ

	HBRUSH HGDIOBJ

	HDC HANDLE

	HFONT HGDIOBJ

	HGDIOBJ HANDLE


	HPALETTE HGDIOBJ


	HRGN HGDIOBJ

)

// http://msdn.microsoft.com/en-us/library/windows/desktop/ms633577.aspx
type WNDCLASSEX struct {
	Size       uint32
	Style      uint32
	WndProc    uintptr
	ClsExtra   int32
	WndExtra   int32
	Instance   HINSTANCE
	Icon       HICON
	Cursor     HCURSOR
	Background HBRUSH
	MenuName   *uint16
	ClassName  *uint16
	IconSm     HICON
}

// http://msdn.microsoft.com/en-us/library/windows/desktop/dd162805.aspx
type POINT struct {
	X, Y int32
}

// http://msdn.microsoft.com/en-us/library/windows/desktop/ms644958.aspx
type MSG struct {
	Hwnd    HWND
	Message uint32
	WParam  uintptr
	LParam  uintptr
	Time    uint32
	Pt      POINT
}


const (
	TickerMSG uint32 = 0x10

	WM_USER                   = 1024

	WM_DESTROY                = 2

	WM_QUIT                   = 18

	WS_EX_APPWINDOW        = 0x00040000

	WS_VISIBLE          = 0x10000000

	COLOR_BACKGROUND              = 1

	WS_POPUP            = 0x80000000


	)

const CW_USEDEFAULT = ^0x7fffffff

type Window struct {
	wc                WNDCLASSEX
	msg               MSG
	die               chan struct{}
	ticker            *time.Ticker
	tickerMillisec    time.Duration
	tickerActive      bool
	handle            HWND
}

func NewWindow(title string, wi, he uint32) (*Window, error) {
	var w Window
	w.die = make(chan struct{}, 1)
	utf16ClassName := syscall.StringToUTF16Ptr("myWinApp")
	w.wc.Size = uint32(unsafe.Sizeof(w.wc))
	w.wc.Style = 0
	w.wc.WndProc = syscall.NewCallback(w.callback)
	w.wc.Background = (HBRUSH)(COLOR_BACKGROUND)
	w.wc.Instance = GetModuleHandle(nil)
	w.wc.ClassName = utf16ClassName
	if RegisterClassEx(&w.wc) == 0 {
		return nil, errors.New("RegisterClassEx error")
	}
	w.handle = CreateWindowEx(WS_EX_APPWINDOW, utf16ClassName, syscall.StringToUTF16Ptr(title), WS_POPUP | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, int(wi), int(he), 0, 0, GetModuleHandle(nil), nil)
	if w.handle == 0 {
		return nil, errors.New("CreateWindowEx error")
	}
	UpdateWindow(w.handle)
	return &w, nil
}

func (w *Window) callback(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr {
	if msg == WM_DESTROY || msg == WM_QUIT {
		PostQuitMessage(0)
		return 0
	}
	if msg == WM_USER + TickerMSG {
		return 0
	}
	return DefWindowProc(hwnd, msg, wParam, lParam)
}

func (w *Window) StartTicker(millisec uint32) {
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()
	// don't start before main loop
	time.Sleep(500 * time.Millisecond)
	w.tickerMillisec = time.Duration(millisec) * time.Millisecond
	w.tickerActive = true
	w.ticker = time.NewTicker(w.tickerMillisec)
	defer w.ticker.Stop()
	for w.tickerActive {
		select {
		case <-w.ticker.C:
			if !PostMessage(w.handle, WM_USER + TickerMSG, 0, 0) {
				fmt.Println("Ticker error. Exiting.")
				w.tickerActive = false
				return
			}
		case <-w.die:
			w.tickerActive = false
			return
		}
	}
}

func (w *Window) Destroy() {
	DestroyWindow(w.handle)
}

func (w *Window) Update()  bool {
	b := GetMessage(&w.msg, 0, 0, 0) > 0
	if !b {
		return false
	}
	TranslateMessage(&w.msg)
	DispatchMessage(&w.msg)
	return true
}


func main() {
	w, err := NewWindow("test window from golang", 440, 440)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer w.Destroy()
	go w.StartTicker(5) // every 5 milliseconds

	for w.Update() {
		
}

}

C++ (g++ app.cpp):

#include <iostream>
#include <windows.h>
#include <thread>
#include <chrono>


void ticker(HWND hwnd)
{
	using namespace std::chrono_literals;
	// don't run before mainloop
	std::this_thread::sleep_for(1000ms);
	bool r = true;
	while(r) {
		if(!PostMessage(hwnd, WM_USER+10, 0, 0)) {
			MessageBox(NULL, "PostMessage error!", "Error!", MB_ICONEXCLAMATION | MB_OK);
			r = false;
			break;
		}
		std::this_thread::sleep_for(5ms); // sleep 5ms and send message
	}
}

// forward decleration
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

int main()
{
MSG msg          = {0};
	HWND handle;

	WNDCLASS wc      = {0}; 
	wc.lpfnWndProc   = WndProc;
	wc.hInstance     = GetModuleHandle(NULL);
	wc.hbrBackground = (HBRUSH)(COLOR_BACKGROUND);
	wc.lpszClassName = "minwindowsapp";
	if( !RegisterClass(&wc) )
		return 1;

	handle = CreateWindow(wc.lpszClassName, "Minimal Windows Application", WS_OVERLAPPEDWINDOW|WS_VISIBLE, 0,0, 640, 480, 0, 0, GetModuleHandle(NULL), NULL);
	if(handle == NULL) {
		return 2;
	}

	std::thread t1(ticker, handle);
	t1.detach();

	while(GetMessage(&msg, NULL, 0, 0 ) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return 0;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch(message) {
	case WM_CLOSE:
		PostQuitMessage(0);
	break;
	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
return 0;
}

What did you expect to see?

The program should not crash.
Ok, a program can crash anytime. But it should not crash sporadically, for no reason. I want to know the problem here.

What did you see instead?

I see a program crashing at unpredictable times.
Sometimes it works correctly for a very long time. Sometimes it crashes within 1-2 minutes.

I think the crash was caused by a component that started running in the runtime. Maybe gc is kicking in or something.

I am apologize for my English. It's not my native language. I hope the codes I shared describe the problem better.

Metadata

Metadata

Assignees

No one assigned

    Labels

    FrozenDueToAgeNeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.WaitingForInfoIssue is not actionable because of missing required information, which needs to be provided.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions