internal/glfw, internal/ui, internal/gamepad, exp/textinput: Win32 wrappers treat a stale last error as a failure
Ebitengine Version
main (915af4cc2, v2.11.0-alpha)
Operating System
- Windows
- macOS
- Linux
- FreeBSD
- OpenBSD
- Android
- iOS
- Nintendo Switch
- PlayStation 5
- Xbox
- Web Browsers
Go Version (paste your go version output)
go version go1.27.0 darwin/arm64
What steps will reproduce the problem?
This is a code-level issue rather than a single reproducible scenario.
The Windows bindings use a common pattern for Win32 functions whose return value is ambiguous (0 can mean either failure or a legitimate result):
r, _, e := procFoo.Call(...)
if r == 0 && !errors.Is(e, windows.ERROR_SUCCESS) {
return fmt.Errorf("...: %w", e)
}There are 64 such sites in internal/glfw/api_windows.go, internal/ui/api_windows.go, internal/gamepad/api_desktop_windows.go, and exp/textinput/api_windows.go.
The e returned by Proc.Call / SyscallN is not a failure indicator. The Go runtime reads the thread's last-error slot after every call and returns it unconditionally. golang.org/x/sys/windows documents this on Proc.Call: the primary return value must be inspected first, per the semantics of the specific function, before consulting the error.
Two things follow:
- A successful call does not clear the last error. The slot holds whatever an earlier failing call on the same thread left behind, and Windows itself sets non-zero codes during successful calls. So when
r == 0is a legitimate result, thee != ERROR_SUCCESShalf of the check fires or not depending on unrelated history, and a spurious error is returned. - Some of the wrapped functions are not documented to set the last error at all (e.g.
GetSystemMetrics,GetClassLongPtrW,ImmAssociateContext), soecarries no information about them even on a real failure.
Sites where 0 is a legitimate return and a stale error can cause a spurious failure include:
_GetSystemMetrics(_SM_REMOTESESSION)ininternal/glfw/win32_init_windows.go: 0 means "not a remote session", the common case. A spurious error abortsplatformInit._GetClassLongPtrW/_GetClassLongWwithGCLP_HICONSMininternal/glfw/win32_window_windows.go: the window class sets no small icon, so 0 is the normal value. A spurious error failsebiten.SetWindowIcon(nil)._ImmAssociateContextininternal/ui/api_windows.goandexp/textinput/api_windows.go: the wrapper checksewithout looking at the return value at all. The previous HIMC is legitimately NULL, and the API is not documented to set the last error. A spurious error fails window creation inafterWindowCreation._SetWindowLongPtrWininternal/gamepad/api_desktop_windows.goandexp/textinput/api_windows.go: the previous value can be 0. Microsoft's documentation for this function prescribes callingSetLastError(0)before the call to disambiguate._GetWindowLongWand similar getters ininternal/glfw/api_windows.go.
What is the expected result?
The wrappers should decide success or failure from the return value according to each API's documented contract, and consult the last error only after a documented failure. For APIs where 0 is ambiguous, either:
- call
SetLastError(0)before the call and checkGetLastErrorafterwards (the form Microsoft prescribes forSetWindowLongPtr), or - do not treat 0 as an error at those sites at all, as upstream GLFW does for e.g.
GetClassLongPtr.
Wrappers for functions that never set the last error should not consult e.
What happens instead?
A stale last-error value from an earlier, unrelated call can turn a successful call into a returned error. Which sites are affected in practice depends on what the thread did before, so failures are intermittent and environment-dependent.
Anything else you feel useful to add?
This is a sweep over the whole Windows binding layer rather than a single fix, so it is filed separately from the individual OS API fixes landed recently (e.g. 915af4cc2, which kept the existing pattern for consistency).
Filed by Claude (Claude Code) on behalf of @hajimehoshi.
Source: hajimehoshi/ebiten