Hello, fellow Gophers!
I’m excited to share a venture I’ve been engaged on: a utterly Cgo-free Go binding for the Slint UI engine, at present engaged on each Home windows and Linux (amd64).
For years, constructing native desktop GUIs in Go has pressured builders into tough trade-offs: both pull in a heavy net stack (Chromium/Webview through 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, stunning reactive DSL, and a royalty-free desktop license for proprietary apps. Nevertheless, bringing a Rust engine into Go with out Cgo appeared almost inconceivable at first. Right here is how it’s constructed beneath the hood.
The Technical Problem: Why Zero-Cgo is Arduous
In commonplace Go improvement, linking international native libraries requires Cgo. To remove Cgo fully, 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 through
.syso**
The Go compiler (cmd/compileandcmd/hyperlink) natively acknowledges.syso(System Object) information positioned inside a bundle listing and hyperlinks them straight with out invoking GCC or Clang. However merely dropping a.sysointo the bundle doesn’t work out of the field.
The Secret Sauce: Bridging the ABI Hole with Trampolines
The core cause .syso linking fails for international features is the ABI mismatch:
- Go makes use of its personal inner calling conference (traditionally Plan 9 ABI on x86, transitioning to register-based ABI0/ABIInternal).
- This conference is basically incompatible with commonplace 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 perform requires an meeting trampoline that saves the Go runtime state, aligns the stack in line 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 a whole bunch of meeting trampolines by hand throughout a number of OS targets is nearly inconceivable to keep up.
To resolve this, the trampolines and Go perform stubs are mechanically 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 bundle passes to the linker.) - Lightning-fast compilation: Instantaneous builds utilizing pure Go toolchains.
- Single static binaries: Slint markup information are merged through our bundling software and embedded straight into the binary utilizing
go:embed.
Presently, the binding makes use of the Slint dynamic interpreter to guage UI markup at runtime. We hope that sooner or later the Slint group will present first-class native Go code technology, however for now, this gives 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 options are very welcome!
“Go perform stubs are mechanically generated by the Hike language compiler (a toolchain I developed).”
Very cool stuff!
It’s additionally attention-grabbing to see within the venture that UI occasions are dealt with in form of queues, if I’m not improper (Jogs my memory of io_uring( io_uring – Wikipedia ) however used at a better stage).
Thanks for the type phrases, Akilan!
The io_uring comparability is an attention-grabbing take, however the architectural cause for this express polling loop goes even deeper into how we handle the OS essential thread.
In commonplace Slint bindings (like JavaScript), occasion dealing with makes use of callbacks the place handler features are invoked implicitly. Whereas handy, that strategy 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 express occasion polling mannequin:
- Deterministic thread execution: Occasion processing is assured to run explicitly on the principle thread (locked through runtime.LockOSThread()).
- Coexistence with main-thread-only OS sources: Since polling can return or trip when idle, builders can interleave different OS-level operations that strictly require the principle thread (reminiscent of 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 sturdy for real-world Go functions!
Thanks lots for taking the time for writing the response! (Very a lot appreciated). The deterministic thread execution is absolutely attention-grabbing.
“builders can interleave different OS-level operations that strictly require the principle thread”
I see, does this imply your entire library runs on a single thread ?
Not fairly! It doesn’t prohibit your entire library or your utility to a single thread.
Somewhat, solely the UI occasion loop and OS-level windowing interactions are explicitly pinned to the OS essential thread (through runtime.LockOSThread()). That is strictly to fulfill OS necessities (like Win32 message pumps, macOS Cocoa runloops, or GPU context guidelines) the place sure sources should not migrate throughout native threads.
The whole lot else in Go behaves utterly usually:
- You may freely spawn background goroutines throughout a number of OS threads for heavy computation, file I/O, or networking.
- Background goroutines can simply talk with the UI loop utilizing commonplace Go channels or protected synchronization primitives.
- Slint’s inner engine may handle its personal background employees or rendering duties the place relevant.
Briefly, it ensures thread-affinity the place the OS calls for it, whereas preserving Go’s full M:N concurrency mannequin intact in all places else!
I see, bought it!
Thanks, Will certainly maintain a watch out on the venture for future updates (I’m engaged on UI system for analysing flight information from a number of classes of flight simulations which can be open supply by the mid of subsequent 12 months. I’ve added it to my record of libraries to doubtlessly use by early Jan 2027).

