Por que aplicativos Wails crashem em produção e como obter controle de baixo nível
٢٦ يوليو ٢٠٢٦
0
Computing/SoftwareComments (0)
Log in to leave a comment
No posts yet
Log in to leave a comment
No posts yet
Ao criar aplicativos desktop com Go, o Wails é uma escolha atraente. Como não empacota o Chromium inteiro como o Electron faz, ele é leve e rápido. No entanto, assim que você vai além dos tutoriais e tenta criar um serviço real, você bate direto de frente com uma parede. A memória CGo começa a vazar, e a webview se comporta de maneira diferente no Windows e no Mac.
Para desenvolvedores backend sem experiência em integração com C/C++ ou Objective-C, esse ponto é um verdadeiro obstáculo. Se você não conseguir resolver os vazamentos de memória e a fragmentação de webviews específica do SO ocultos por trás dos exemplos reluzentes da documentação oficial, o implante em produção torna-se impossível.
O equívoco mais comum ao usar CGo é a expectativa de que o coletor de lixo do Go cuidará do espaço C. Obviamente, ele não cuidará. A memória alocada com C.CString ou C.malloc permanece no espaço C, consumindo memória até que a aplicação seja encerrada.
Também é preciso ter cuidado ao passar fatias (slices) do Go para funções C. Passar o endereço do próprio cabeçalho da fatia causará corrupção de memória. É seguro passar o endereço real do primeiro elemento, ou seja, unsafe.Pointer(&slice[0]). Ao chamar código Objective-C no macOS, não confie no ARC. Os objetos criados dentro do loop de threads CGo continuam se acumulando no NSAutoreleasePool. É necessário envolvê-los explicitamente em um bloco @autoreleasepool { ... } para limpá-los imediatamente.
Em um ambiente Windows, não há necessidade de carregar o compilador CGo (MinGW) como uma dependência desnecessária. Você pode simplesmente chamar DLLs diretamente com o pacote syscall sem a sobrecarga do CGo. O código para carregar dwmapi.dll e ativar o modo escuro é mais simples do que parece.
`go
// system_windows.go
//go:build windows
package native
import (
"syscall"
"unsafe"
)
var (
modDwmApi = syscall.NewLazyDLL("dwmapi.dll")
procDwmSetWindowAttribute = modDwmApi.NewProc("DwmSetWindowAttribute")
)
const DWMWA_USE_IMMERSIVE_DARK_MODE = 20
func SetWindowsDarkMode(hwnd uintptr, enable bool) error {
var val int32
if enable {
val = 1
}
ret, _, err := procDwmSetWindowAttribute.Call(
hwnd,
uintptr(DWMWA_USE_IMMERSIVE_DARK_MODE),
uintptr(unsafe.Pointer(&val)),
uintptr(unsafe.Sizeof(val)),
)
if ret != 0 {
return err
}
return nil
}
`
Ao criar um módulo de controle nativo, primeiro defina uma interface comum (system_interface.go). Em seguida, adicione a lógica Objective-C na implementação do macOS (system_darwin.go) juntamente com a diretiva //go:build darwin, e escreva Syscalls em Pure-Go juntamente com //go:build windows na implementação do Windows para separá-las. Criar o hábito de adicionar defer C.free logo após as alocações CGo já é suficiente para evitar que a aplicação feche por vazamentos de memória.
O Wails utiliza a webview já instalada no sistema operacional. No macOS é o WebKit (Safari), e no Windows é o WebView2 (Chromium). Em troca de reduzir o tamanho do binário para cerca de 15MB, você precisa lidar diretamente com a fragmentação entre os motores de navegação.
Por exemplo, ao definir uma área arrastável em uma janela sem moldura (frameless window), o WebView2 funciona apenas especificando --wails-draggable: drag, mas o WebKit não moverá a janela a menos que você também especifique -webkit-app-region: drag.
Problemas também acontecem no tratamento de eventos. Se você disparar runtime.EventsEmit milhares de vezes por segundo a partir de goroutines Go, a thread única de UI da webview irá travar e a tela congelará. É necessário colocar um buffer no backend para limitar a taxa de eventos a um ciclo de 60fps (cerca de 16ms).
`typescript
// eventPatch.ts
export function applyGlobalUIFixes() {
window.addEventListener('contextmenu', (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (target.tagName !== 'INPUT' && target.tagName !== 'TEXTAREA') {
e.preventDefault();
}
});
window.addEventListener('keydown', (e: KeyboardEvent) => {
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const modifier = isMac ? e.metaKey : e.ctrlKey;
if (e.key === 'F5' || (modifier && e.key.toLowerCase() === 'r')) {
e.preventDefault();
e.stopPropagation();
}
});
}
`
No CSS, declare as propriedades de arrastar para ambos os motores simultaneamente e execute applyGlobalUIFixes() no ponto de entrada do aplicativo (main.ts ou App.tsx). Adicione um temporizador de limitação no emissor de eventos do backend. Com essas medidas simples, a maioria dos comportamentos anômalos causados pelas características das webviews de cada SO estará sob controle.
Aplicativos Wails costumam consumir cerca de 35MB a 50MB de RAM em uso normal. Comparado ao Electron, que consome mais de 200MB, isso é excelente. O problema surge ao transferir grandes arquivos ou dados binários para o frontend.
Se você enviar e receber 50MB de dados usando a vinculação JSON RPC padrão, o uso de RAM pode disparar momentaneamente para mais de 180MB durante o processo de serialização JSON. Para evitar esse fenômeno, você deve implementar streaming HTTP personalizado usando a opção AssetServer.AssetsHandler. Passar dados com uma abordagem Zero-copy sem duplicação de memória permite manter o uso de memória inativa e de processamento na faixa de 22MB a 30MB.
Também é necessário suporte para ambientes de usuários Windows. Para clientes que não possuem o runtime do WebView2 instalado, inclua a flag -webview2 download ao compilar para empacotar o bootstrapper junto.
`yaml
name: Multiplatform Release Build
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: macos-latest
platform: darwin/universal
output_name: OptimizedApp-macOS-Universal
- os: windows-latest
platform: windows/amd64
output_name: OptimizedApp-Windows-Installer
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Wails
run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
- name: Build macOS Universal Binary
if: runner.os == 'macOS'
run: wails build -platform darwin/universal -clean
- name: Build Windows Installer
if: runner.os == 'Windows'
run: |
choco install nsis -y
wails build -platform windows/amd64 -nsis -webview2 download -clean
`
Conecte um http.Handler personalizado ao AssetServer.AssetsHandler em main.go para resolver picos de memória ao lidar com grandes volumes de dados. Em seguida, adicione o pipeline acima em .github/workflows/release.yml. Toda vez que você enviar uma tag, o binário universal do macOS e o instalador NSIS do Windows serão gerados e disponibilizados no GitHub Releases.
Gerenciando a memória de baixo nível de forma direta e absorvendo as diferenças dos motores de webview via código, você conseguirá criar aplicativos desktop extremamente sólidos utilizando o Wails.