Thursday, October 29, 2026
HomeGolangHow we achieved single-binary static linking with out Cgo (Home windows &...

How we achieved single-binary static linking with out Cgo (Home windows & Linux) – Technical Dialogue


Hello, fellow Gophers!

I’m excited to share a mission 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 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 inconceivable at first. Right here is how it’s constructed underneath the hood.


The Technical Problem: Why Zero-Cgo is Arduous

In normal Go improvement, linking international native libraries requires Cgo. To eradicate Cgo completely, two main hurdles needed to be solved:

  1. Self-contained Native Static Library
    Cgo can’t be eliminated if the construct course of nonetheless invokes C compiler headers. We bundled all the Slint engine and its C-API wrapper (SlintCFlat) right into a single, precompiled static archive.
  2. **Direct Linking by way of .syso**
    The Go compiler (cmd/compile and cmd/hyperlink) natively acknowledges .syso (System Object) recordsdata positioned inside a bundle listing and hyperlinks them immediately with out invoking GCC or Clang. However merely dropping a .syso into 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 capabilities 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 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 immediately:

  • Each single API perform requires an meeting trampoline that saves the Go runtime state, aligns the stack based on the host platform’s ABI, strikes parameters from Go’s registers/stack into host registers (e.g., RCX/RDX/R8/R9 on Home windows or RDI/RSI/RDX/RCX on Linux), calls the international image, and marshals return values again to Go.
  • Writing lots of of meeting trampolines by hand throughout a number of OS targets is just about inconceivable to keep up.

To resolve this, the trampolines and Go perform 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 Consequence

By combining pre-packaged .syso archives with compiler-generated meeting trampolines, we obtain:

  • Zero Cgo: CGO_ENABLED=0 builds simply work.(Nevertheless, it’s essential to manually specify the choices that the Slit bundle passes to the linker.)
  • Lightning-fast compilation: Prompt builds utilizing pure Go toolchains.
  • Single static binaries: Slint markup recordsdata are merged by way of our bundling software and embedded immediately into the binary utilizing go:embed.

At the moment, 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 technology, however for now, this gives a quick, light-weight, and fashionable 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 perform stubs are routinely generated by the Hike language compiler (a toolchain I developed).”

Very cool stuff!

It’s additionally fascinating to see within the mission that UI occasions are dealt with in type of queues, if I’m not flawed (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 fascinating take, however the architectural cause for this express polling loop goes even deeper into how we handle the OS predominant thread.

In normal Slint bindings (like JavaScript), occasion dealing with makes use of callbacks the place handler capabilities are invoked implicitly. Whereas handy, that strategy obscures how the primary thread waits for occasions and turns the primary occasion loop right into a black field.

On this Go binding, we deliberately selected an express occasion polling mannequin:

  1. Deterministic thread execution: Occasion processing is assured to run explicitly on the primary thread (locked by way of runtime.LockOSThread()).
  2. 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 primary thread (equivalent to Win32 COM/OLE routines, macOS Cocoa loops, or native platform sources) alongside GUI occasion processing.

Giving full management of the primary loop again to the developer is what makes this structure so sturdy for real-world Go purposes!

Thanks rather a lot for taking the time for writing the response! (Very a lot appreciated). The deterministic thread execution is admittedly fascinating.

“builders can interleave different OS-level operations that strictly require the primary thread”

I see, does this imply all the library runs on a single thread ?

Not fairly! It doesn’t limit all the library or your utility to a single thread.

Fairly, solely the UI occasion loop and OS-level windowing interactions are explicitly pinned to the OS predominant thread (by way of 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.

Every part 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 normal Go channels or secure synchronization primitives.
  • Slint’s inner engine also can handle its personal background staff 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 all over the place else!

I see, received it!

Thanks, Will certainly maintain an eye fixed out on the mission for future updates (I’m engaged on UI system for analysing flight knowledge from a number of classes of flight simulations which might be open supply by the mid of subsequent yr. I’ve added it to my record of libraries to doubtlessly use by early Jan 2027).

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments