mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-15 15:15:51 +01:00
* perf(v3): optimize JSON processing and reduce allocations in hot paths - Switch to goccy/go-json for method binding, events, and HTTP transport (21-63% faster, 40-60% less memory for method calls) - Optimize BoundMethod struct layout to reduce padding (144 -> 136 bytes) - Cache isVariadic flag at registration to avoid reflect call per invocation - Use stack-allocated buffer for method arguments (<=8 args) - Optimize result collection to avoid slice allocation for single return values - Use sync.Map for MIME cache to improve concurrent read performance - Use buffer pool for HTTP transport request body reading - Lazily allocate CloseNotify channel in content type sniffer - Remove debug CSS logging from asset server - Add comprehensive benchmark tests (build tag: bench) Performance improvements for BoundMethod.Call: - SimpleCall: 1290ns -> 930ns (28% faster), 240B -> 80B (67% less memory) - ComplexCall: 10500ns -> 3900ns (63% faster), 1192B -> 1020B (14% less) - VariadicCall: 3460ns -> 1600ns (54% faster), 512B -> 289B (44% less) * perf(v3): add max size limit to buffer pool to prevent memory bloat Buffers larger than 512KB are not returned to the pool, allowing GC to reclaim memory after large requests (e.g., base64 encoded images). * perf(v3): remove mimetype library dependency, saving ~208KB binary size - Replace github.com/wailsapp/mimetype with expanded extension map + stdlib - Expand MIME type map from 16 to 50+ common web formats (fonts, audio, video, etc.) - Add comprehensive test suite validating MIME detection for all web formats - Use http.DetectContentType as fallback for unknown extensions - Actual binary size reduction: 1.2MB (11MB -> 9.8MB in test app) * perf(v3): migrate all runtime code to goccy/go-json Migrate remaining encoding/json usages to goccy/go-json in: - pkg/application (android, darwin, ios, single_instance, webview_window) - pkg/services (kvstore, notifications on all platforms) - internal/assetserver/webview (request/response handling) - internal/runtime and internal/capabilities Note: encoding/json (110KB) remains in binary because: 1. goccy/go-json imports it for interface compatibility (json.Marshaler, etc.) 2. log/slog (stdlib) uses it for JSON output The performance benefit is in the hot paths which now use the faster library. * perf(v3): replace gopkg.in/ini.v1 with minimal .desktop file parser Replace the gopkg.in/ini.v1 dependency with a purpose-built minimal parser for Linux .desktop files. The new parser: - Only extracts the Exec key from [Desktop Entry] section (all we need) - Follows the Desktop Entry Specification - Has comprehensive test coverage (40 tests) including: - All major file managers (Nautilus, Dolphin, Thunar, PCManFM, Caja, Nemo) - Edge cases (UTF-8, special chars, comments, empty files, etc.) - Buffer limit handling Binary size reduction: 45KB (10.22MB -> 10.18MB) * perf(v3): remove samber/lo from runtime code, saving ~310KB binary size Replace samber/lo with Go 1.21+ stdlib slices package and minimal internal helpers in all runtime code paths. This removes 80 transitive dependencies from the production binary. Changes: - Create internal/sliceutil package with Unique and FindMapKey helpers - Replace lo.Without with slices.DeleteFunc in event handling - Replace lo.Ternary with inline if/else in Windows code - Replace lo.Uniq with sliceutil.Unique for feature flags - Replace lo.FindKey with sliceutil.FindMapKey for method aliases - Replace lo.Filter with slices.DeleteFunc in event listeners - Replace lo.Must with inline panic in w32 package Binary size: 10.18MB -> 9.87MB (~310KB / 3% reduction) Note: CLI tools still use samber/lo since they don't affect production binary size. The application_debug.go file also retains lo usage as it has //go:build !production tag. * fix: address CodeRabbit review comments - Use application/x-typescript MIME type (not IANA-registered text/typescript) - Fix potential panic in mimetype_stdlib_test.go for short MIME strings - Use cached isVariadic flag in bindings_optimized_bench_test.go * fix: initialize goccy/go-json decoder early to fix Windows test failure On Windows, goccy/go-json's type address calculation can fail if the decoder is first invoked during test execution rather than at init time. Force early initialization by unmarshaling a []int during package init. See: https://github.com/goccy/go-json/issues/474 * 📝 Add docstrings to `v3/performance-improvements` (#4844) * fix: initialize goccy/go-json decoder early to fix Windows test failure On Windows, goccy/go-json's type address calculation can fail if the decoder is first invoked during test execution rather than at init time. Force early initialization by unmarshaling a []int during package init. See: https://github.com/goccy/go-json/issues/474 * 📝 Add docstrings to `v3/performance-improvements` Docstrings generation was requested by @leaanthony. * https://github.com/wailsapp/wails/pull/4843#issuecomment-3703472562 The following files were modified: * `v3/internal/assetserver/common.go` * `v3/internal/assetserver/content_type_sniffer.go` * `v3/internal/assetserver/mimecache.go` * `v3/internal/fileexplorer/desktopfile.go` * `v3/internal/fileexplorer/fileexplorer_linux.go` * `v3/internal/sliceutil/sliceutil.go` * `v3/pkg/application/application_ios.go` * `v3/pkg/application/bindings.go` * `v3/pkg/application/ios_runtime_ios.go` * `v3/pkg/w32/window.go` --------- Co-authored-by: Lea Anthony <lea.anthony@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
377 lines
No EOL
9.8 KiB
Go
377 lines
No EOL
9.8 KiB
Go
//go:build windows
|
|
|
|
package w32
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"unsafe"
|
|
)
|
|
|
|
var (
|
|
user32 = syscall.NewLazyDLL("user32.dll")
|
|
getSystemMenu = user32.NewProc("GetSystemMenu")
|
|
getMenuProc = user32.NewProc("GetMenu")
|
|
enableMenuItem = user32.NewProc("EnableMenuItem")
|
|
findWindow = user32.NewProc("FindWindowW")
|
|
sendMessage = user32.NewProc("SendMessageW")
|
|
vkKeyScan = user32.NewProc("VkKeyScanW") // Use W version for Unicode
|
|
)
|
|
|
|
func VkKeyScan(ch uint16) uint16 {
|
|
ret, _, _ := syscall.SyscallN(
|
|
vkKeyScan.Addr(),
|
|
uintptr(ch),
|
|
)
|
|
return uint16(ret)
|
|
}
|
|
|
|
const (
|
|
WMCOPYDATA_SINGLE_INSTANCE_DATA = 1542
|
|
)
|
|
|
|
type COPYDATASTRUCT struct {
|
|
DwData uintptr
|
|
CbData uint32
|
|
LpData uintptr
|
|
}
|
|
|
|
var Fatal func(error)
|
|
|
|
const (
|
|
GCLP_HBRBACKGROUND int32 = -10
|
|
GCLP_HICON int32 = -14
|
|
)
|
|
|
|
type WINDOWPOS struct {
|
|
HwndInsertAfter HWND
|
|
X int32
|
|
Y int32
|
|
Cx int32
|
|
Cy int32
|
|
Flags uint32
|
|
}
|
|
|
|
func ExtendFrameIntoClientArea(hwnd uintptr, extend bool) error {
|
|
// -1: Adds the default frame styling (aero shadow and e.g. rounded corners on Windows 11)
|
|
// Also shows the caption buttons if transparent ant translucent but they don't work.
|
|
// 0: Adds the default frame styling but no aero shadow, does not show the caption buttons.
|
|
// 1: Adds the default frame styling (aero shadow and e.g. rounded corners on Windows 11) but no caption buttons
|
|
// are shown if transparent ant translucent.
|
|
var margins MARGINS
|
|
if extend {
|
|
margins = MARGINS{1, 1, 1, 1} // Only extend 1 pixel to have the default frame styling but no caption buttons
|
|
}
|
|
if err := dwmExtendFrameIntoClientArea(hwnd, &margins); err != nil {
|
|
return fmt.Errorf("DwmExtendFrameIntoClientArea failed: %s", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func IsVisible(hwnd uintptr) bool {
|
|
ret, _, _ := procIsWindowVisible.Call(hwnd)
|
|
return ret != 0
|
|
}
|
|
|
|
func IsWindowFullScreen(hwnd uintptr) bool {
|
|
wRect := GetWindowRect(hwnd)
|
|
m := MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY)
|
|
var mi MONITORINFO
|
|
mi.CbSize = uint32(unsafe.Sizeof(mi))
|
|
if !GetMonitorInfo(m, &mi) {
|
|
return false
|
|
}
|
|
return wRect.Left == mi.RcMonitor.Left &&
|
|
wRect.Top == mi.RcMonitor.Top &&
|
|
wRect.Right == mi.RcMonitor.Right &&
|
|
wRect.Bottom == mi.RcMonitor.Bottom
|
|
}
|
|
|
|
func IsWindowMaximised(hwnd uintptr) bool {
|
|
style := uint32(getWindowLong(hwnd, GWL_STYLE))
|
|
return style&WS_MAXIMIZE != 0
|
|
}
|
|
func IsWindowMinimised(hwnd uintptr) bool {
|
|
style := uint32(getWindowLong(hwnd, GWL_STYLE))
|
|
return style&WS_MINIMIZE != 0
|
|
}
|
|
|
|
func RestoreWindow(hwnd uintptr) {
|
|
showWindow(hwnd, SW_RESTORE)
|
|
}
|
|
|
|
func ShowWindowMaximised(hwnd uintptr) {
|
|
showWindow(hwnd, SW_MAXIMIZE)
|
|
}
|
|
func ShowWindowMinimised(hwnd uintptr) {
|
|
showWindow(hwnd, SW_MINIMIZE)
|
|
}
|
|
|
|
func SetApplicationIcon(hwnd uintptr, icon HICON) {
|
|
setClassLongPtr(hwnd, GCLP_HICON, icon)
|
|
}
|
|
|
|
func SetBackgroundColour(hwnd uintptr, r, g, b uint8) {
|
|
col := uint32(r) | uint32(g)<<8 | uint32(b)<<16
|
|
hbrush, _, _ := procCreateSolidBrush.Call(uintptr(col))
|
|
setClassLongPtr(hwnd, GCLP_HBRBACKGROUND, hbrush)
|
|
}
|
|
|
|
func IsWindowNormal(hwnd uintptr) bool {
|
|
return !IsWindowMaximised(hwnd) && !IsWindowMinimised(hwnd) && !IsWindowFullScreen(hwnd)
|
|
}
|
|
|
|
func setClassLongPtr(hwnd uintptr, param int32, val uintptr) bool {
|
|
proc := procSetClassLongPtr
|
|
if strconv.IntSize == 32 {
|
|
/*
|
|
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setclasslongptrw
|
|
Note: To write code that is compatible with both 32-bit and 64-bit Windows, use SetClassLongPtr.
|
|
When compiling for 32-bit Windows, SetClassLongPtr is defined as a call to the SetClassLong function
|
|
|
|
=> We have to do this dynamically when directly calling the DLL procedures
|
|
*/
|
|
proc = procSetClassLong
|
|
}
|
|
|
|
ret, _, _ := proc.Call(
|
|
hwnd,
|
|
uintptr(param),
|
|
val,
|
|
)
|
|
return ret != 0
|
|
}
|
|
|
|
func getWindowLong(hwnd uintptr, index int) int32 {
|
|
ret, _, _ := procGetWindowLong.Call(
|
|
hwnd,
|
|
uintptr(index))
|
|
|
|
return int32(ret)
|
|
}
|
|
|
|
func showWindow(hwnd uintptr, cmdshow int) bool {
|
|
ret, _, _ := procShowWindow.Call(
|
|
hwnd,
|
|
uintptr(cmdshow))
|
|
return ret != 0
|
|
}
|
|
|
|
func stripNulls(str string) string {
|
|
// Split the string into substrings at each null character
|
|
substrings := strings.Split(str, "\x00")
|
|
|
|
// Join the substrings back into a single string
|
|
strippedStr := strings.Join(substrings, "")
|
|
|
|
return strippedStr
|
|
}
|
|
|
|
func MustStringToUTF16Ptr(input string) *uint16 {
|
|
input = stripNulls(input)
|
|
result, err := syscall.UTF16PtrFromString(input)
|
|
if err != nil {
|
|
Fatal(err)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// MustStringToUTF16uintptr converts input to a NUL-terminated UTF-16 buffer and returns its pointer as a uintptr.
|
|
// It first removes any internal NUL characters from input, then converts the result to a UTF-16 pointer.
|
|
// The function panics if the conversion fails.
|
|
func MustStringToUTF16uintptr(input string) uintptr {
|
|
input = stripNulls(input)
|
|
ret, err := syscall.UTF16PtrFromString(input)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return uintptr(unsafe.Pointer(ret))
|
|
}
|
|
|
|
// MustStringToUTF16 converts s to UTF-16 encoding, stripping any embedded NULs and panicking on error.
|
|
//
|
|
// The returned slice is suitable for Windows API calls that expect a UTF-16 encoded string.
|
|
func MustStringToUTF16(input string) []uint16 {
|
|
input = stripNulls(input)
|
|
ret, err := syscall.UTF16FromString(input)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// StringToUTF16 converts input to a UTF-16 encoded, NUL-terminated []uint16 suitable for Windows API calls.
|
|
// It first removes any embedded NUL ('\x00') characters from input. The returned slice is NUL-terminated;
|
|
// an error is returned if the conversion fails.
|
|
func StringToUTF16(input string) ([]uint16, error) {
|
|
input = stripNulls(input)
|
|
return syscall.UTF16FromString(input)
|
|
}
|
|
|
|
func CenterWindow(hwnd HWND) {
|
|
windowInfo := getWindowInfo(hwnd)
|
|
frameless := windowInfo.IsPopup()
|
|
|
|
info := GetMonitorInfoForWindow(hwnd)
|
|
workRect := info.RcWork
|
|
screenMiddleW := workRect.Left + (workRect.Right-workRect.Left)/2
|
|
screenMiddleH := workRect.Top + (workRect.Bottom-workRect.Top)/2
|
|
var winRect *RECT
|
|
if !frameless {
|
|
winRect = GetWindowRect(hwnd)
|
|
} else {
|
|
winRect = GetClientRect(hwnd)
|
|
}
|
|
winWidth := winRect.Right - winRect.Left
|
|
winHeight := winRect.Bottom - winRect.Top
|
|
windowX := screenMiddleW - (winWidth / 2)
|
|
windowY := screenMiddleH - (winHeight / 2)
|
|
SetWindowPos(hwnd, HWND_TOP, int(windowX), int(windowY), int(winWidth), int(winHeight), SWP_NOSIZE)
|
|
}
|
|
|
|
func getWindowInfo(hwnd HWND) *WINDOWINFO {
|
|
var info WINDOWINFO
|
|
info.CbSize = uint32(unsafe.Sizeof(info))
|
|
GetWindowInfo(hwnd, &info)
|
|
return &info
|
|
}
|
|
|
|
func GetMonitorInfoForWindow(hwnd HWND) *MONITORINFO {
|
|
currentMonitor := MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST)
|
|
var info MONITORINFO
|
|
info.CbSize = uint32(unsafe.Sizeof(info))
|
|
GetMonitorInfo(currentMonitor, &info)
|
|
return &info
|
|
}
|
|
|
|
type WindowProc func(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr
|
|
|
|
var windowClasses = make(map[string]HINSTANCE)
|
|
var windowClassesLock sync.Mutex
|
|
|
|
func getWindowClass(name string) (HINSTANCE, bool) {
|
|
windowClassesLock.Lock()
|
|
defer windowClassesLock.Unlock()
|
|
result, exists := windowClasses[name]
|
|
return result, exists
|
|
}
|
|
|
|
func setWindowClass(name string, instance HINSTANCE) {
|
|
windowClassesLock.Lock()
|
|
defer windowClassesLock.Unlock()
|
|
windowClasses[name] = instance
|
|
}
|
|
|
|
func RegisterWindow(name string, proc WindowProc) (HINSTANCE, error) {
|
|
classInstance, exists := getWindowClass(name)
|
|
if exists {
|
|
return classInstance, nil
|
|
}
|
|
applicationInstance := GetModuleHandle("")
|
|
if applicationInstance == 0 {
|
|
return 0, fmt.Errorf("get module handle failed")
|
|
}
|
|
|
|
var wc WNDCLASSEX
|
|
wc.Size = uint32(unsafe.Sizeof(wc))
|
|
wc.WndProc = syscall.NewCallback(proc)
|
|
wc.Instance = applicationInstance
|
|
wc.Icon = LoadIconWithResourceID(0, uint16(IDI_APPLICATION))
|
|
wc.Cursor = LoadCursorWithResourceID(0, uint16(IDC_ARROW))
|
|
wc.Background = COLOR_BTNFACE + 1
|
|
wc.ClassName = MustStringToUTF16Ptr(name)
|
|
|
|
atom := RegisterClassEx(&wc)
|
|
if atom == 0 {
|
|
panic(syscall.GetLastError())
|
|
}
|
|
|
|
setWindowClass(name, applicationInstance)
|
|
|
|
return applicationInstance, nil
|
|
}
|
|
|
|
func FlashWindow(hwnd HWND, enabled bool) {
|
|
var flashInfo FLASHWINFO
|
|
flashInfo.CbSize = uint32(unsafe.Sizeof(flashInfo))
|
|
flashInfo.Hwnd = hwnd
|
|
if enabled {
|
|
flashInfo.DwFlags = FLASHW_ALL | FLASHW_TIMERNOFG
|
|
} else {
|
|
flashInfo.DwFlags = FLASHW_STOP
|
|
}
|
|
_, _, _ = procFlashWindowEx.Call(uintptr(unsafe.Pointer(&flashInfo)))
|
|
}
|
|
|
|
func EnumChildWindows(hwnd HWND, callback func(hwnd HWND, lparam LPARAM) LRESULT) LRESULT {
|
|
r, _, _ := procEnumChildWindows.Call(hwnd, syscall.NewCallback(callback), 0)
|
|
return r
|
|
}
|
|
|
|
func DisableCloseButton(hwnd HWND) error {
|
|
hSysMenu, _, err := getSystemMenu.Call(hwnd, 0)
|
|
if hSysMenu == 0 {
|
|
return err
|
|
}
|
|
|
|
r1, _, err := enableMenuItem.Call(hSysMenu, SC_CLOSE, MF_BYCOMMAND|MF_DISABLED|MF_GRAYED)
|
|
if r1 == 0 {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func EnableCloseButton(hwnd HWND) error {
|
|
hSysMenu, _, err := getSystemMenu.Call(hwnd, 0)
|
|
if hSysMenu == 0 {
|
|
return err
|
|
}
|
|
|
|
r1, _, err := enableMenuItem.Call(hSysMenu, SC_CLOSE, MF_BYCOMMAND|MF_ENABLED)
|
|
if r1 == 0 {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func FindWindowW(className, windowName *uint16) HWND {
|
|
ret, _, _ := findWindow.Call(
|
|
uintptr(unsafe.Pointer(className)),
|
|
uintptr(unsafe.Pointer(windowName)),
|
|
)
|
|
return HWND(ret)
|
|
}
|
|
|
|
func SendMessageToWindow(hwnd HWND, msg string) {
|
|
// Convert data to UTF16 string
|
|
dataUTF16, err := StringToUTF16(msg)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Prepare COPYDATASTRUCT
|
|
cds := COPYDATASTRUCT{
|
|
DwData: WMCOPYDATA_SINGLE_INSTANCE_DATA,
|
|
CbData: uint32((len(dataUTF16) * 2) + 1), // +1 for null terminator
|
|
LpData: uintptr(unsafe.Pointer(&dataUTF16[0])),
|
|
}
|
|
|
|
// Send message to first instance
|
|
_, _, _ = procSendMessage.Call(
|
|
hwnd,
|
|
WM_COPYDATA,
|
|
0,
|
|
uintptr(unsafe.Pointer(&cds)),
|
|
)
|
|
}
|
|
|
|
// GetMenu retrieves a handle to the menu assigned to the specified window
|
|
func GetMenu(hwnd HWND) HMENU {
|
|
ret, _, _ := getMenuProc.Call(hwnd)
|
|
return ret
|
|
} |