#3700·ebiten

internal/glfw, internal/ui, internal/gamepad, exp/textinput: Win32 wrappers treat a stale last error as a failure

Author: hajimehoshiCreated Sep 10, 2026Updated Sep 10, 2026
Labelsbugos:windows

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):

go
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:

  1. 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 == 0 is a legitimate result, the e != ERROR_SUCCESS half of the check fires or not depending on unrelated history, and a spurious error is returned.
  2. Some of the wrapped functions are not documented to set the last error at all (e.g. GetSystemMetrics, GetClassLongPtrW, ImmAssociateContext), so e carries 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) in internal/glfw/win32_init_windows.go: 0 means "not a remote session", the common case. A spurious error aborts platformInit.
  • _GetClassLongPtrW / _GetClassLongW with GCLP_HICONSM in internal/glfw/win32_window_windows.go: the window class sets no small icon, so 0 is the normal value. A spurious error fails ebiten.SetWindowIcon(nil).
  • _ImmAssociateContext in internal/ui/api_windows.go and exp/textinput/api_windows.go: the wrapper checks e without 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 in afterWindowCreation.
  • _SetWindowLongPtrW in internal/gamepad/api_desktop_windows.go and exp/textinput/api_windows.go: the previous value can be 0. Microsoft's documentation for this function prescribes calling SetLastError(0) before the call to disambiguate.
  • _GetWindowLongW and similar getters in internal/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 check GetLastError afterwards (the form Microsoft prescribes for SetWindowLongPtr), 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.