Monday, November 2, 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 undertaking I’ve been engaged on: a utterly Cgo-free Go binding for the Slint UI engine, presently 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 internet stack (Chromium/Webview by way of Wails), take care of 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 unimaginable at first. Right here is how it’s constructed below the hood.


The Technical Problem: Why Zero-Cgo is Laborious

In customary Go growth, linking international native libraries requires Cgo. To eradicate Cgo solely, 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 the complete 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) information positioned inside a package deal listing and hyperlinks them straight with out invoking GCC or Clang. However merely dropping a .syso into 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 features 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 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 response to 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 unimaginable to take care of.

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 write down a single line of meeting or C.


The Outcome

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

  • Zero Cgo: CGO_ENABLED=0 builds 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 instrument 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 someday the Slint staff will present first-class native Go code era, however for now, this offers a quick, light-weight, and trendy GUI expertise for Go builders.

Take a look at the repository and examples right here:

Suggestions, questions in regards to the linking internals, and solutions are very welcome!


“Go operate stubs are robotically generated by the Hike language compiler (a toolchain I developed).”

Very cool stuff!

It’s additionally fascinating to see within the undertaking 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 degree).

Thanks for the sort phrases, Akilan!

The io_uring comparability is an fascinating take, however the architectural purpose for this express polling loop goes even deeper into how we handle the OS most important thread.

In customary Slint bindings (like JavaScript), occasion dealing with makes use of callbacks the place handler features 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 express occasion polling mannequin:

  1. Deterministic thread execution: Occasion processing is assured to run explicitly on the principle thread (locked by way of runtime.LockOSThread()).
  2. Coexistence with main-thread-only OS sources: Since polling can return or day out when idle, builders can interleave different OS-level operations that strictly require the principle thread (similar 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 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 basically fascinating.

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

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

Not fairly! It doesn’t limit the complete 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 most important 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 customary Go channels or protected synchronization primitives.
  • Slint’s inside engine also can handle its personal background employees or rendering duties the place relevant.

Briefly, it ensures thread-affinity the place the OS calls for it, whereas holding Go’s full M:N concurrency mannequin intact in every single place else!

I see, received it!

Thanks, Will certainly preserve an eye fixed out on the undertaking for future updates (I’m engaged on UI system for analysing flight knowledge from a number of periods of flight simulations which shall be open supply by the mid of subsequent 12 months. I’ve added it to my checklist of libraries to probably use by early Jan 2027).

Hello Akilan,

As a fast replace following our dialogue on direct GPU/vector rendering wants:

To assist advance the Go graphics ecosystem—significantly for recreation engines and customized GUI toolkits past Slint—I’ve simply launched WGPUStatic, a Cgo-free static binding of Rust’s wgpu-native for Go:

It permits linking wgpu-native straight into standalone Go executables by way of .syso and Plan 9 meeting stubs with out runtime DLL dependencies (presently examined and working on Home windows amd64).

Since your flight simulation telemetry and evaluation undertaking will seemingly require high-performance, uncooked GPU rendering pipelines alongside UI widgets, I assumed this is likely to be helpful to your architectural issues. Better of luck along with your undertaking!

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments