Ouvrir une fenêtre avec HTML en Go coûte presque toujours un péage en C.
Ou bien vous activez le cgo et vous traînez une toolchain C dans le projet, ou quelqu’un intègre une .dll/.so/.dylib compilée dans l’exécutable et l’extrait en disque dès la première exécution. Les deux fonctionnent. Et les deux vous enlèvent exactement ce que j’apprécie dans l’écosystème: le go build qui croise vers trois systèmes sans réfléchir, la construction reproductible, le go install qui tourne pour quiconque clone le dépôt. C’est la première chose qui disparaît lorsque le C entre.
Le Glaze a commencé par opter pour la deuxième option. C’était un fork du go-webview: il intégrait la bibliothèque C++ du webview/webview compilée, extrayait le blob dans un répertoire temporaire et le chargeait à partir de là. Il y avait même une vérification BLAKE2b-256 du fichier extrait par rapport aux octets intégrés, pour que personne ne troque la lib sur le disque entre l’extraction et le chargement. Ça fonctionnait. Mais c’est beaucoup de cérémonies pour ouvrir une fenêtre: un blob binaire par plateforme, généré dans un job CI, extrait à l’exécution, et un hash pour valider un fichier que j’avais moi-même écrit.
La bascule a été de cesse de charger une lib propre et de passer à l’appel de la WebView que le système d’exploitation a déjà. macOS vient avec WKWebView. Windows 10/11 apporte le runtime de WebView2. Sous Linux, c’est un WebKitGTK système, que vous déclarez comme dépendance plutôt que d’empaqueter. Dans aucun des cas la lib native n’a pas besoin d’êtres associée à l’exécutable. Elle est déjà présente sur la machine.
Ce qui manquait, c’était de parler avec elle sans cgo. C’est là qu’entre en jeu le purego.
purego est une FFI sans cgo
L’idée de purego est d’ouvrir une bibliothèque partagée au lancement et d’appeler une fonction C directement, sans import "C", sans compilateur C sur le chemin. Sous Linux et macOS, c’est dlopen/dlsym qui s’en occupe en coulisses :
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")
Après cela webkitWebViewNew() devient une fonction Go ordinaire qui appelle le symbole C. Aucun stade de compilation, aucun header.
Sous Windows, il n’y a pas de Dlopen – on résout les symboles avec LoadLibrary et GetProcAddress et on s’inscrit de la même façon. Pour le callback C qui appelle Go en retour, il existe NewCallback, qui sur Windows délègue au syscall.NewCallback de la stdlib et ajuste la convention stdcall. Et pour macOS il y a le paquet objc qui parle avec le runtime Objective‑C: enregistrer une classe, créer une méthode en Go que AppKit appelle en retour, les blocks, la struct passée par valeur. On peut monter une NSWindow avec un WKWebView tout entier en Go.
Chaque système a sa malédiction
Ce n’est pas gratuit. Vous échangez un blob binaire contre la réécriture du backend sur chaque système, et chacun en réclame son prix.
macOS a été le plus net. Cocoa via objc, les délégués enregistrés comme classes Go, autorelease pool en main. Fastidieux, mais direct.
Linux est une ABI C brute, le terrain où purego brille. Le détail agaçant, c’est la version. La WebView d’origine choisit GTK3 ou GTK4 au moment de la compilation; au runtime, vous n’avez pas ce luxe. Alors Glaze détecte quelle pile est installée – libwebkitgtk-6.0 (GTK4) ou libwebkit2gtk-4.1/4.0 (GTK3) – et il échange les appels, car l’arité de certaines fonctions a changé entre les deux versions.
Windows fut l’enfer. WebView2 n’est pas une API C bête et méchante, c’est du COM. Vous appelez une méthode par indice dans une vtable de pointeurs, vous implémentez les interfaces de callback de votre côté (une vtable montée en Go avec QueryInterface/AddRef/Release/Invoke), et vous gérez aussi le compte de références d’un objet que le garbage collector ne connaît pas. Et pour maintenir le « zéro DLL » sans embarquer le WebView2Loader.dll, Glaze réimplémente la découverte du loader : il retrouve la DLL du runtime via le registre Windows et appelle un export interne d’Edge pour créer l’environnement. C’est bien ce que fait le loader officiel en coulisses. Le prix, c’est que cet export est interne et non documenté, et Microsoft peut le renommer ou le retirer. Si cela disparaît, vous obtenez une erreur claire plutôt qu’une explosion brutale.
Une règle traverse les trois backends: aucun pointeur Go brut ne passe vers le C. Le moteur est identifié par un identifiant entier stocké dans une map côté Go, et ce qui est envoyé vers le C n’est que cet entier. Un pointeur Go donné au C invite le GC à bouger la mémoire en dessous.
La base de tests est l’Intégration Continue (CI)
Il y a un détail peu reluisant: je développe sur macOS. Je n’avais pas moyen d’exécuter Linux et Windows à la main. Et les bugs d’ABI n’apparaissent pas en cross-compilation – ça se compile joliment et ça plante à l’exécution.
Ainsi la base de tests est devenue un conteneur et une CI: GitHub Actions exécutant les trois systèmes (macOS, Windows, Linux en GTK3 et GTK4, amd64 et arm64), avec une révision adversaire du code COM avant de dépenser des cycles CI sur Windows. C’est ainsi que sont apparus, par exemple, que g_signal_handlers_disconnect_by_data n’est pas un symbole exporté, c’est une macro du GObject – symbole indéfini qui n’apparaît qu’à l’exécution, dans un conteneur. Ou que passer un RECT par valeur au WebView2 fonctionne sur amd64 et échoue sur arm64, car le struct de 16 octets passe par référence dans l’un et est empaqueté dans deux registres dans l’autre.
Ce qui reste
En fin de compte, ce qui demeure est exactement ce que je voulais dès le départ :
CGO_ENABLED=0partout ;- cross-compiler pour les trois systèmes directement à partir du
go build, sans toolchain C ; - un binaire qui n’emporte aucune lib native avec lui ;
go install github.com/crgimenes/glazequi fonctionne pour ceux qui ont cloné, sans rituel.
Ce dont on dépend désormais, c’est que le runtime soit présent: rien de plus sur macOS, un WebKitGTK sur Linux, le Runtime Edge WebView2 sur Windows (qui est déjà inclus dans Windows 10/11). C’est un échange honnête, une dépendance système déclarée plutôt qu’une dépendance embarquée et dissimulée.
Le Glaze n’“utilise” plus le webview/webview en runtime. C’est devenu un port: la même idée, réécrite en Go pur sur purego, sans un seul octet de C++ dans le binaire. Remplacer un blob compilé par trois backends écrits à la main demande plus de travail, et c’était le cas. Mais ce qui sort du go build parle désormais COM sur Windows, Objective‑C sur macOS et GTK sur Linux sans aucun import "C" nulle part.




