Dev (Back & Front)ARTIGO

Glaze, WebView desktop em Go sem CGO

Glaze, WebView desktop em Go sem CGO
Imagem: Cesar Gimenes

Abrir uma janela com HTMLHTML45 conteúdosA importância do HTML e CSS para quem trabalha com UI Design e Design SystemProduto & UX · dez 2024Como hostear seu site HTML gratuitamente com GitHub PagesDev (Back & Front) · jun 2025SQL Server – Como criar um versionamento de código das suas Stored Procedures em HTML e com comentários da alteraçãoData · nov 2020Ver tudo em Dev (Back & Front) em Go quase sempre cobra um pedágio em C.

Ou você liga o cgo e arrasta uma toolchain C pro projeto, ou alguém embute uma .dll/.so/.dylib compilada no binário e extrai em disco na primeira execução. Os dois funcionam. E os dois te tiram justamente o que eu gosto no ecossistema: o go build que cross-compila pra três sistemas sem pensar, o build reprodutível, o go install que roda pra quem clonou o repo. É a primeira coisa que vai embora quando o C entra.

Glaze começou fazendo a segunda opção. Era um fork do go-webview: embutia a biblioteca C++ do webview/webview compilada, extraía o blob pra um diretório temporário e carregava de lá. Tinha até verificação BLAKE2b-256 do arquivo extraído contra os bytes embutidos, pra ninguém trocar a lib no disco entre a extração e o load. Funcionava. Mas é muita cerimônia pra abrir uma janela: um blob binário por plataforma, gerado num job de CI, extraído em runtime, e um hash pra validar um arquivo que eu mesmo acabei de escrever.

A virada foi parar de carregar uma lib minha e passar a chamar a WebView que o sistema operacional já tem. macOS vem com WKWebView. Windows 10/11 traz o runtime do WebView2. No LinuxLinux34 conteúdosKali Linux em um Servidor VPS: como, quando e por que usar?DevSecOps · dez 2024Construindo um Windows Service ou Linux Daemon com Worker Service & .NET Core – Parte 2Dev (Back & Front) · jul 2020Criando uma WebApi utilizando .NET, Linux e VSCodeDev (Back & Front) · ago 2019Ver tudo em DevSecOps é um WebKitGTK de sistema, que você declara como dependência em vez de empacotar. Em nenhum dos casos a lib nativa precisa ir junto no binário. Ela já está na máquina.

O que faltava era falar com ela sem cgo. É aí que entra o purego.

purego é FFI sem cgo

A ideia do purego é abrir uma biblioteca compartilhada em runtime e chamar função de C direto, sem import "C", sem compilador C no caminho. No Linux e no macOS é dlopen/dlsym por baixo:

go
lib, _ := purego.Dlopen("libwebkitgtk-6.0.so.4", purego.RTLD_NOW|purego.RTLD_GLOBAL)

var webkitWebViewNew func() uintptr
purego.RegisterLibFunc(&webkitWebViewNew, lib, "webkit_web_view_new")

Depois disso webkitWebViewNew() é uma função Go normal que chama o símbolo de C. Nenhum estágio de compilação, nenhum header.

No Windows não tem Dlopen – você resolve os símbolos com LoadLibrary e GetProcAddress e registra do mesmo jeito. Pra callback de C que chama Go de volta tem o NewCallback, que no Windows delega pro syscall.NewCallback da stdlib e acerta a convenção stdcall. E pro macOS existe o pacote objc, que fala com o runtime do Objective-C: registrar classe, criar um método em Go que o AppKit chama de volta, blocks, struct passado por valor. Dá pra montar um NSWindow com um WKWebView dentro inteiro em Go.

Cada sistema tem a sua maldição

Não é de graça. Você troca um blob binário por reimplementar o backend em cada sistema, e cada um cobra o seu preço.

macOS foi o mais limpo. Cocoa via objc, os delegates registrados como classe Go, autorelease pool na mão. Trabalhoso, mas direto.

Linux é C ABI puro, o terreno onde o purego brilha. O detalhe chato é versão. O webview original escolhe GTK3 ou GTK4 em tempo de compilação; em runtime você não tem esse luxo. Então o Glaze detecta qual stack está instalado – libwebkitgtk-6.0 (GTK4) ou libwebkit2gtk-4.1/4.0 (GTK3) – e troca as chamadas, porque a aridade de algumas funções mudou entre as duas versões.

Windows foi o inferno. WebView2 não é uma API C boba, é COM. Você chama método por índice numa vtable de ponteiros, implementa as interfaces de callback do lado de cá (uma vtable montada em Go com QueryInterface/AddRef/Release/Invoke), e ainda cuida do ref-count de um objeto que o garbage collector não conhece. E pra manter o “zero DLL” sem embutir o WebView2Loader.dll, o Glaze reimplementa a descoberta do loader: acha a DLL do runtime pelo registro do Windows e chama um export interno do Edge pra criar o ambiente. É o que o loader oficial faz por baixo. O preço é que esse export é interno e não-documentado, e a Microsoft pode renomear ou remover. Se sumir, dá erro claro em vez de explodir feio.

Uma regra atravessa os três backends: nenhum ponteiro Go cruza pra C. O engine é identificado por um id inteiro guardado num mapa do lado Go, e o que vai pro C é só o inteiro. Ponteiro de Go na mão do C é convite pro GC mover a memória embaixo dele.

O banco de testes é CI

Tem um detalhe nada glamouroso: eu desenvolvo no macOS. Linux e Windows eu não tinha como rodar na mão. E bug de ABI não aparece no cross-compile – compila lindo e quebra na execução.

Então o banco de testes virou container e CI: GitHub Actions rodando os três sistemas (macOS, Windows, Linux em GTK3 e GTK4, amd64 e arm64), mais revisão adversarial do código COM antes de gastar ciclo de CI no Windows. Foi assim que apareceu, por exemplo, que g_signal_handlers_disconnect_by_data não é símbolo exportado, é macro do GObject – undefined symbol que só estoura em runtime, dentro de um container. Ou que passar um RECT por valor pro WebView2 funciona no amd64 e quebra no arm64, porque o struct de 16 bytes vai por referência num e empacotado em dois registradores no outro.

O que sobra

No fim, o que fica é o que eu queria desde o começo:

  • CGO_ENABLED=0 em tudo;
  • cross-compile pros três sistemas direto do go build, sem toolchain C;
  • binário que não carrega nenhuma lib nativa junto;
  • go install github.com/crgimenes/glaze funcionando pra quem clonou, sem ritual.

O que você passa a depender é o runtime estar presente: nada extra no macOS, um WebKitGTK no Linux, o Edge WebView2 Runtime no Windows (que já vem no 10/11). É uma troca honesta, dependência de sistema declarada no lugar de dependência embutida e disfarçada.

O Glaze não “usa” mais o webview/webview em runtime. Virou um port: a mesma ideia, reescrita em Go puro sobre purego, sem um byte de C++ no binário. Trocar um blob compilado por três backends escritos à mão é mais trabalho, e foi. Mas o que sai do go build agora fala COM no Windows, objc no macOS e GTK no Linux sem um import "C" em lugar nenhum.

Trabalha com tecnologia desde a década de 90. Já atuou na área de educação e participou de projetos de mobilidade de grande volume para laboratórios farmacêuticos. Criou games tanto para PC, como para iOS. Hoje está direcionando seus esforços em plataformas de Sistemas Embarcados, IoT, microservices e cloud computing. É um entusiasta de tecnologias como Golang e Docker.

Ver perfil