Hello, fellow Gophers!
I’m excited to share a mission I’ve been engaged on: a fully 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 pressured builders into troublesome trade-offs: both pull in a heavy net stack (Chromium/Webview by way of Wails), cope with immediate-mode UI quirks, or wrestle with Cgo dependencies that break quick construct occasions and painless cross-compilation.
Slint is a contemporary, declarative GUI toolkit written in Rust with tiny useful resource footprints, lovely reactive DSL, and a royalty-free desktop license for proprietary apps. Nevertheless, bringing a Rust engine into Go with out Cgo appeared almost unattainable at first. Right here is how it’s constructed underneath the hood.
The Technical Problem: Why Zero-Cgo is Exhausting
In customary Go growth, linking overseas native libraries requires Cgo. To eradicate Cgo completely, 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 complete 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) information 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 overseas 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 customary OS calling conventions (Microsoft x64 ABI on Home windows, System V AMD64 ABI on Linux).
- Register allocations, stack alignment, and shadow house differ fully.
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 keeping 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 overseas image, and marshals return values again to Go. - Writing a whole bunch of meeting trampolines by hand throughout a number of OS targets is nearly unattainable to keep up.
To unravel this, the trampolines and Go operate stubs are robotically generated by the Hike language compiler (a toolchain I developed). You don’t have to jot down 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, you need to manually specify the choices that the Slit package deal passes to the linker.) - Lightning-fast compilation: Prompt builds utilizing pure Go toolchains.
- Single static binaries: Slint markup information are merged by way of our bundling software and embedded straight into the binary utilizing
go:embed.
At present, the binding makes use of the Slint dynamic interpreter to judge UI markup at runtime. We hope that in the future the Slint staff will present first-class native Go code era, however for now, this supplies a quick, light-weight, and trendy GUI expertise for Go builders.
Try the repository and examples right here:
Suggestions, questions in regards to the linking internals, and recommendations are very welcome!
“Go operate stubs are robotically generated by the Hike language compiler (a toolchain I developed).”
Very cool stuff!
It’s additionally attention-grabbing to see within the mission that UI occasions are dealt with in form of queues, if I’m not fallacious (Jogs my memory of io_uring( io_uring – Wikipedia ) however used at the next stage).
Thanks for the type phrases, Akilan!
The io_uring comparability is an attention-grabbing take, however the architectural purpose for this specific polling loop goes even deeper into how we handle the OS major thread.
In customary 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 assets: Since polling can return or day trip when idle, builders can interleave different OS-level operations that strictly require the principle thread (akin to Win32 COM/OLE routines, macOS Cocoa loops, or native platform assets) 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!
Thanks so much for taking the time for writing the response! (Very a lot appreciated). The deterministic thread execution is de facto attention-grabbing.
“builders can interleave different OS-level operations that strictly require the principle thread”
I see, does this imply your complete library runs on a single thread ?

