Hello, fellow Gophers!
I’m excited to share a challenge I’ve been engaged on: a utterly Cgo-free Go binding for the Slint UI engine, at the moment engaged on each Home windows and Linux (amd64).
For years, constructing native desktop GUIs in Go has compelled builders into troublesome trade-offs: both pull in a heavy internet stack (Chromium/Webview by way of Wails), cope with immediate-mode UI quirks, or wrestle with Cgo dependencies that break quick construct instances and painless cross-compilation.
Slint is a contemporary, declarative GUI toolkit written in Rust with tiny useful resource footprints, stunning reactive DSL, and a royalty-free desktop license for proprietary apps. Nevertheless, bringing a Rust engine into Go with out Cgo appeared practically not possible at first. Right here is how it’s constructed beneath the hood.
The Technical Problem: Why Zero-Cgo is Arduous
In normal Go growth, linking international native libraries requires Cgo. To eradicate Cgo totally, two main hurdles needed to be solved:
- Self-contained Native Static Library
Cgo can’t be eliminated if the construct course of nonetheless invokes C compiler headers. We bundled your entire Slint engine and its C-API wrapper (SlintCFlat) right into a single, precompiled static archive. - **Direct Linking by way of
.syso**
The Go compiler (cmd/compileandcmd/hyperlink) natively acknowledges.syso(System Object) recordsdata positioned inside a package deal listing and hyperlinks them straight with out invoking GCC or Clang. However merely dropping a.sysointo the package deal doesn’t work out of the field.
The Secret Sauce: Bridging the ABI Hole with Trampolines
The core purpose .syso linking fails for international capabilities is the ABI mismatch:
- Go makes use of its personal inside calling conference (traditionally Plan 9 ABI on x86, transitioning to register-based ABI0/ABIInternal).
- This conference is essentially incompatible with normal OS calling conventions (Microsoft x64 ABI on Home windows, System V AMD64 ABI on Linux).
- Register allocations, stack alignment, and shadow house differ utterly.
To make the Go compiler name into Slint’s C-API straight:
- Each single API operate requires an meeting trampoline that saves the Go runtime state, aligns the stack in accordance with the host platform’s ABI, strikes parameters from Go’s registers/stack into host registers (e.g.,
RCX/RDX/R8/R9on Home windows orRDI/RSI/RDX/RCXon Linux), calls the international image, and marshals return values again to Go. - Writing tons of of meeting trampolines by hand throughout a number of OS targets is nearly not possible to keep up.
To resolve this, the trampolines and Go operate stubs are routinely generated by the Hike language compiler (a toolchain I developed). You don’t have to put in writing a single line of meeting or C.
The End result
By combining pre-packaged .syso archives with compiler-generated meeting trampolines, we obtain:
- Zero Cgo:
CGO_ENABLED=0builds simply work.(Nevertheless, it’s essential to manually specify the choices that the Slit package deal passes to the linker.) - Lightning-fast compilation: Instantaneous builds utilizing pure Go toolchains.
- Single static binaries: Slint markup recordsdata are merged by way of our bundling software and embedded straight into the binary utilizing
go:embed.
Presently, the binding makes use of the Slint dynamic interpreter to judge UI markup at runtime. We hope that at some point the Slint staff will present first-class native Go code era, however for now, this supplies a quick, light-weight, and fashionable GUI expertise for Go builders.
Take a look at the repository and examples right here:
Suggestions, questions in regards to the linking internals, and ideas are very welcome!
“Go operate stubs are routinely generated by the Hike language compiler (a toolchain I developed).”
Very cool stuff!
It’s additionally fascinating to see within the challenge that UI occasions are dealt with in form of queues, if I’m not unsuitable (Jogs my memory of io_uring( io_uring – Wikipedia ) however used at a better stage).
Thanks for the sort phrases, Akilan!
The io_uring comparability is an fascinating take, however the architectural purpose for this specific polling loop goes even deeper into how we handle the OS foremost thread.
In normal Slint bindings (like JavaScript), occasion dealing with makes use of callbacks the place handler capabilities are invoked implicitly. Whereas handy, that method obscures how the principle thread waits for occasions and turns the principle occasion loop right into a black field.
On this Go binding, we deliberately selected an specific occasion polling mannequin:
- Deterministic thread execution: Occasion processing is assured to run explicitly on the principle thread (locked by way of runtime.LockOSThread()).
- Coexistence with main-thread-only OS sources: Since polling can return or day trip when idle, builders can interleave different OS-level operations that strictly require the principle thread (comparable to Win32 COM/OLE routines, macOS Cocoa loops, or native platform sources) alongside GUI occasion processing.
Giving full management of the principle loop again to the developer is what makes this structure so strong for real-world Go functions!

